// 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) }
// 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 }