decred.org/dcrdex@v1.0.5/server/apidata/apidata.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package apidata 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "sync" 10 "sync/atomic" 11 "time" 12 13 "decred.org/dcrdex/dex" 14 "decred.org/dcrdex/dex/candles" 15 "decred.org/dcrdex/dex/msgjson" 16 "decred.org/dcrdex/server/comms" 17 "decred.org/dcrdex/server/matcher" 18 ) 19 20 var ( 21 // Our internal millisecond representation of the bin sizes. 22 binSizes []uint64 23 started uint32 24 ) 25 26 // DBSource is a source of persistent data. DBSource is used to prime the 27 // caches at startup. 28 type DBSource interface { 29 LoadEpochStats(base, quote uint32, caches []*candles.Cache) error 30 LastCandleEndStamp(base, quote uint32, candleDur uint64) (uint64, error) 31 InsertCandles(base, quote uint32, dur uint64, cs []*candles.Candle) error 32 } 33 34 // MarketSource is a source of market information. Markets are added after 35 // construction but before use using the AddMarketSource method. 36 type MarketSource interface { 37 EpochDuration() uint64 38 Base() uint32 39 Quote() uint32 40 } 41 42 // BookSource is a source of order book information. The BookSource is added 43 // after construction but before use. 44 type BookSource interface { 45 Book(mktName string) (*msgjson.OrderBook, error) 46 } 47 48 type cacheWithStoredTime struct { 49 *candles.Cache 50 lastStoredEndStamp uint64 // protected by DataAPI.cacheMtx 51 } 52 53 // DataAPI is a data API backend. 54 type DataAPI struct { 55 db DBSource 56 epochDurations map[string]uint64 57 bookSource BookSource 58 59 spotsMtx sync.RWMutex 60 spots map[string]json.RawMessage 61 62 cacheMtx sync.RWMutex 63 marketCaches map[string]map[uint64]*cacheWithStoredTime 64 } 65 66 // NewDataAPI is the constructor for a new DataAPI. 67 func NewDataAPI(dbSrc DBSource, registerHTTP func(route string, handler comms.HTTPHandler)) *DataAPI { 68 s := &DataAPI{ 69 db: dbSrc, 70 epochDurations: make(map[string]uint64), 71 spots: make(map[string]json.RawMessage), 72 marketCaches: make(map[string]map[uint64]*cacheWithStoredTime), 73 } 74 75 if atomic.CompareAndSwapUint32(&started, 0, 1) { 76 registerHTTP(msgjson.SpotsRoute, s.handleSpots) 77 registerHTTP(msgjson.CandlesRoute, s.handleCandles) 78 registerHTTP(msgjson.OrderBookRoute, s.handleOrderBook) 79 } 80 return s 81 } 82 83 // AddMarketSource should be called before any markets are running. 84 func (s *DataAPI) AddMarketSource(mkt MarketSource) error { 85 mktName, err := dex.MarketName(mkt.Base(), mkt.Quote()) 86 if err != nil { 87 return err 88 } 89 epochDur := mkt.EpochDuration() 90 s.epochDurations[mktName] = epochDur 91 binCaches := make(map[uint64]*cacheWithStoredTime, len(binSizes)+1) 92 cacheList := make([]*candles.Cache, 0, len(binSizes)+1) 93 for _, binSize := range append([]uint64{epochDur}, binSizes...) { 94 cache := candles.NewCache(candles.CacheSize, binSize) 95 lastCandleEndStamp, err := s.db.LastCandleEndStamp(mkt.Base(), mkt.Quote(), cache.BinSize) 96 if err != nil { 97 return fmt.Errorf("LastCandleEndStamp: %w", err) 98 } 99 c := &cacheWithStoredTime{cache, lastCandleEndStamp} 100 cacheList = append(cacheList, cache) 101 binCaches[binSize] = c 102 } 103 err = s.db.LoadEpochStats(mkt.Base(), mkt.Quote(), cacheList) 104 if err != nil { 105 return err 106 } 107 s.cacheMtx.Lock() 108 s.marketCaches[mktName] = binCaches 109 s.cacheMtx.Unlock() 110 return nil 111 } 112 113 // SetBookSource should be called before the first call to handleBook. 114 func (s *DataAPI) SetBookSource(bs BookSource) { 115 s.bookSource = bs 116 } 117 118 // ReportEpoch should be called by every Market after every match cycle to 119 // report their epoch stats. 120 func (s *DataAPI) ReportEpoch(base, quote uint32, epochIdx uint64, stats *matcher.MatchCycleStats) (*msgjson.Spot, error) { 121 mktName, err := dex.MarketName(base, quote) 122 if err != nil { 123 return nil, err 124 } 125 126 // Add the candlestick. 127 addCandle := func() (change24 float64, vol24, high24, low24 uint64, err error) { 128 s.cacheMtx.Lock() 129 defer s.cacheMtx.Unlock() 130 mktCaches := s.marketCaches[mktName] 131 if mktCaches == nil { 132 return 0, 0, 0, 0, fmt.Errorf("unknown market %q", mktName) 133 } 134 epochDur := s.epochDurations[mktName] 135 startStamp := epochIdx * epochDur 136 endStamp := startStamp + epochDur 137 var cache5min *cacheWithStoredTime 138 const fiveMins = uint64(time.Minute * 5 / time.Millisecond) 139 candle := &candles.Candle{ 140 StartStamp: startStamp, 141 EndStamp: endStamp, 142 MatchVolume: stats.MatchVolume, 143 QuoteVolume: stats.QuoteVolume, 144 HighRate: stats.HighRate, 145 LowRate: stats.LowRate, 146 StartRate: stats.StartRate, 147 EndRate: stats.EndRate, 148 } 149 for dur, cache := range mktCaches { 150 if dur == fiveMins { 151 cache5min = cache 152 } 153 cache.Add(candle) 154 155 // Check if any candles need to be inserted. 156 // Don't insert epoch candles. 157 if cache.BinSize == epochDur { 158 continue 159 } 160 161 newCandles := cache.CompletedCandlesSince(cache.lastStoredEndStamp) 162 if len(newCandles) == 0 { 163 continue 164 } 165 if err := s.db.InsertCandles(base, quote, cache.BinSize, newCandles); err != nil { 166 return 0, 0, 0, 0, fmt.Errorf("InsertCandles: %w", err) 167 } 168 cache.lastStoredEndStamp = newCandles[len(newCandles)-1].EndStamp 169 } 170 if cache5min == nil { 171 return 0, 0, 0, 0, fmt.Errorf("no 5 minute cache") 172 } 173 change24, vol24, high24, low24 = cache5min.Delta(time.Now().Add(-time.Hour * 24)) 174 return 175 } 176 177 change24, vol24, high24, low24, err := addCandle() 178 if err != nil { 179 return nil, err 180 } 181 182 // Encode the spot price. 183 spot := &msgjson.Spot{ 184 Stamp: uint64(time.Now().UnixMilli()), 185 BaseID: base, 186 QuoteID: quote, 187 Rate: stats.EndRate, 188 Change24: change24, 189 Vol24: vol24, 190 High24: high24, 191 Low24: low24, 192 } 193 194 s.spotsMtx.Lock() 195 s.spots[mktName], err = json.Marshal(spot) 196 s.spotsMtx.Unlock() 197 return spot, err 198 } 199 200 // handleSpots implements comms.HTTPHandler for the /spots endpoint. 201 func (s *DataAPI) handleSpots(any) (any, error) { 202 s.spotsMtx.RLock() 203 defer s.spotsMtx.RUnlock() 204 spots := make([]json.RawMessage, 0, len(s.spots)) 205 for _, spot := range s.spots { 206 spots = append(spots, spot) 207 } 208 return spots, nil 209 } 210 211 // handleCandles implements comms.HTTPHandler for the /candles endpoints. 212 func (s *DataAPI) handleCandles(thing any) (any, error) { 213 req, ok := thing.(*msgjson.CandlesRequest) 214 if !ok { 215 return nil, fmt.Errorf("candles request unparseable") 216 } 217 218 if req.NumCandles == 0 { 219 req.NumCandles = candles.DefaultCandleRequest 220 } else if req.NumCandles > candles.CacheSize { 221 return nil, fmt.Errorf("requested numCandles %d exceeds maximum request size %d", req.NumCandles, candles.CacheSize) 222 } 223 224 mkt, err := dex.MarketName(req.BaseID, req.QuoteID) 225 if err != nil { 226 return nil, fmt.Errorf("error parsing market for %d - %d", req.BaseID, req.QuoteID) 227 } 228 229 binSizeDuration, err := time.ParseDuration(req.BinSize) 230 if err != nil { 231 return nil, fmt.Errorf("error parsing binSize") 232 } 233 binSize := uint64(binSizeDuration / time.Millisecond) 234 235 s.cacheMtx.RLock() 236 defer s.cacheMtx.RUnlock() 237 marketCaches := s.marketCaches[mkt] 238 if marketCaches == nil { 239 return nil, fmt.Errorf("market %s not known", mkt) 240 } 241 242 cache := marketCaches[binSize] 243 if cache == nil { 244 return nil, fmt.Errorf("no data available for binSize %s", req.BinSize) 245 } 246 247 return cache.WireCandles(req.NumCandles), nil 248 } 249 250 // handleOrderBook implements comms.HTTPHandler for the /orderbook endpoints. 251 func (s *DataAPI) handleOrderBook(thing any) (any, error) { 252 req, ok := thing.(*msgjson.OrderBookSubscription) 253 if !ok { 254 return nil, fmt.Errorf("unparseable orderbook request") 255 } 256 257 mkt, err := dex.MarketName(req.Base, req.Quote) 258 if err != nil { 259 return nil, fmt.Errorf("can't parse requested market") 260 } 261 return s.bookSource.Book(mkt) 262 } 263 264 func init() { 265 for _, s := range candles.BinSizes { 266 dur, err := time.ParseDuration(s) 267 if err != nil { 268 panic("error parsing bin size '" + s + "': " + err.Error()) 269 } 270 binSizes = append(binSizes, uint64(dur/time.Millisecond)) 271 } 272 }