示例#1
0
// Query for a list of transactions. Admin-only for now.
func handleQueryTrades(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	if !user.IsAdmin(c) {
		http.Error(w, "Action requires admin privileges", http.StatusForbidden)
		return
	}
	w.Header().Set("Access-Control-Allow-Origin", "*")

	limitStr := r.FormValue("limit")
	var limit int
	if limitStr == "" {
		limit = 1000
	} else if n, err := strconv.ParseUint(limitStr, 10, 14); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	} else {
		limit = int(n)
	}

	articleStr := r.FormValue("article")
	var article bitwrk.ArticleId
	if articleStr == "" {
		http.Error(w, "article argument missing", http.StatusNotFound)
		return
	} else if err := checkArticle(c, articleStr); err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	} else {
		article = bitwrk.ArticleId(articleStr)
	}

	periodStr := r.FormValue("period")
	if periodStr == "" {
		periodStr = "1d"
	} else if !resolutionExists(periodStr) {
		http.Error(w, "period unknown", http.StatusNotFound)
		return
	}
	period := resolutionByName(periodStr)

	// If begin is not given, calculate
	beginStr := r.FormValue("begin")
	var begin time.Time
	if beginStr == "" {
		begin = time.Now().Add(-period.interval)
	} else if t, err := time.Parse(time.RFC3339, beginStr); err != nil {
		http.Error(w, "Invalid begin time", http.StatusNotFound)
		return
	} else {
		begin = t
	}

	end := begin.Add(period.interval)

	unit := money.MustParseUnit("mBTC")
	buffer := new(bytes.Buffer)
	fmt.Fprintf(buffer, "{\"begin\": %#v, \"end\": %#v, \"unit\": \"%v\", \"data\": [\n",
		begin.Format(time.RFC3339), end.Format(time.RFC3339), unit)
	count := 0
	priceSum := money.MustParse("BTC 0")
	feeSum := money.MustParse("BTC 0")

	firstLine := true
	handler := func(key string, tx bitwrk.Transaction) {
		var comma string
		if firstLine {
			firstLine = false
			comma = " "
		} else {
			comma = ","
		}
		fmt.Fprintf(buffer, "%v[% 5d, %#v, %v, %v, %v, \"%v\", \"%v\", %#v]\n", comma,
			count,
			tx.Matched.Format(time.RFC3339Nano), tx.Matched.UnixNano()/1000000,
			tx.Price.Format(unit, false),
			tx.Fee.Format(unit, false),
			tx.State, tx.Phase, key)

		priceSum = priceSum.Add(tx.Price)
		feeSum = feeSum.Add(tx.Fee)
		count++
	}
	if err := db.QueryTransactions(c, limit, article, begin, end, handler); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		c.Errorf("Error querying transactions: %v", err)
		return
	}
	fmt.Fprintf(buffer, "], \"price_sum\": %v,  \"fee_sum\": %v}\n",
		priceSum.Format(unit, false), feeSum.Format(unit, false))

	// Write result back to requester
	w.Header().Set("Content-Type", "application/json")
	buffer.WriteTo(w)
}
示例#2
0
// Returns prices for trades between 'begin' and 'end', in resolution 'res', recursing from tile size 'tile' down to the
// most appropriate tile size, employing caching on the go.
// Assumes that 'begin' is aligned with the current tile size.
func queryPrices(c appengine.Context, article bitwrk.ArticleId, tile, res resolution, begin, end time.Time) ([]timeslot, error) {
	finest := res.finestTileResolution()
	if tile.finerThan(finest) {
		panic("Tile size too small")
	}

	// Does the interval fit into a single tile? If no, fan out and don't cache.
	if begin.Add(tile.interval).Before(end) {
		result := make([]timeslot, 0)
		count := 0
		for begin.Before(end) {
			tileEnd := begin.Add(tile.interval)
			if tileEnd.After(end) {
				tileEnd = end
			}

			if r, err := queryPrices(c, article, tile, res, begin, tileEnd); err != nil {
				return nil, err
			} else {
				c.Infof("Fan-out to tile #%v returned %v slots", count, len(r))
				result = append(result, r...)
			}
			begin = tileEnd
			count++
		}
		c.Infof("Fan-out to %v tiles returned %v slots", count, len(result))
		return result, nil
	}

	// First try to answer from cache
	key := fmt.Sprintf("prices-tile-%v/%v-%v-%v", tile.name, res.name, begin.Format(time.RFC3339), article)
	if item, err := memcache.Get(c, key); err == nil {
		result := make([]timeslot, 0)
		if err := json.Unmarshal(item.Value, &result); err != nil {
			// Shouldn't happen
			c.Errorf("Couldn't unmarshal memcache entry for: %v : %v", key, err)
		} else {
			return result, nil
		}
	}

	// Cache miss. Need to fetch data.
	// If tile size is the smallest for the desired resolution, ask the datastore.
	// Otherwise, recurse with next smaller tile size.
	var result []timeslot
	if tile == res.finestTileResolution() {

		count := 0
		currentSlot := timeslot{
			Begin: begin,
			End:   begin.Add(res.interval),
		}
		result = make([]timeslot, 0)

		// Handler function that is called for every transaction in the current tile
		handler := func(key string, tx bitwrk.Transaction) {
			// Flush current interval to result list
			for currentSlot.End.Before(tx.Matched) {
				if currentSlot.Count > 0 {
					result = append(result, currentSlot)
					count += currentSlot.Count
				}
				currentSlot.advance()
			}
			currentSlot.addPrice(tx.Price)
		}

		// Query database
		if err := db.QueryTransactions(c, 10000, article, begin, end, handler); err != nil {
			return nil, err
		}

		// Flush last interval
		if currentSlot.Count > 0 {
			result = append(result, currentSlot)
			count += currentSlot.Count
		}

		c.Infof("QueryTransactions from %v to %v: %v slots/%v tx",
			begin, end, len(result), count)
	} else if r, err := queryPrices(c, article, tile.nextFiner(), res, begin, end); err != nil {
		return nil, err
	} else {
		result = r
	}

	// Before returning, update the cache.
	item := memcache.Item{Key: key}
	if data, err := json.Marshal(result); err != nil {
		// Shouldn't happen
		c.Errorf("Error marshalling result: %v", err)
	} else {
		item.Value = data
	}

	// Tiles very close to now expire after 10 seconds
	if begin.Add(tile.interval).After(time.Now().Add(-2 * time.Minute)) {
		item.Expiration = 10 * time.Second
	}

	if err := memcache.Add(c, &item); err != nil {
		c.Errorf("Error caching item for %v: %v", key, err)
	}

	return result, nil
}