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  }