decred.org/dcrdex@v1.0.5/dex/candles/candles.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 candles
     5  
     6  import (
     7  	"time"
     8  
     9  	"decred.org/dcrdex/dex/msgjson"
    10  	"decred.org/dcrdex/dex/utils"
    11  )
    12  
    13  const (
    14  	// DefaultCandleRequest is the number of candles to return if the request
    15  	// does not specify otherwise.
    16  	DefaultCandleRequest = 50
    17  	// CacheSize is the default cache size. Also represents the maximum number
    18  	// of candles that can be requested at once.
    19  	CacheSize = 1000
    20  )
    21  
    22  var (
    23  	// BinSizes is the default bin sizes for candlestick data sets. Exported for
    24  	// use in the 'config' response. Internally, we will parse these to uint64
    25  	// milliseconds.
    26  	BinSizes = []string{"24h", "1h", "5m"}
    27  )
    28  
    29  // Candle is a report about the trading activity of a market over some specified
    30  // period of time. Candles are managed with a Cache, which takes into account
    31  // bin sizes and handles candle addition.
    32  type Candle = msgjson.Candle
    33  
    34  // Cache is a sized cache of candles. Cache provides methods for adding to the
    35  // cache and reading cache data out. Candles is a typical slice until it reaches
    36  // capacity, when it becomes a "circular array" to avoid re-allocations.
    37  type Cache struct {
    38  	Candles []Candle
    39  	BinSize uint64
    40  	cap     int
    41  	// cursor will be the index of the last inserted candle.
    42  	cursor int
    43  }
    44  
    45  // NewCache is a constructor for a Cache.
    46  func NewCache(cap int, binSize uint64) *Cache {
    47  	return &Cache{
    48  		cap:     cap,
    49  		BinSize: binSize,
    50  	}
    51  }
    52  
    53  // Add adds a new candle TO THE END of the Cache. The caller is responsible to
    54  // ensure that candles added with Add are always newer than the last candle
    55  // added.
    56  func (c *Cache) Add(candle *Candle) {
    57  	sz := len(c.Candles)
    58  	if sz == 0 {
    59  		c.Candles = append(c.Candles, *candle)
    60  		return
    61  	}
    62  
    63  	if c.combineCandles(c.Last(), candle) {
    64  		return
    65  	}
    66  	if sz == c.cap { // circular mode
    67  		c.cursor = (c.cursor + 1) % c.cap
    68  		c.Candles[c.cursor] = *candle
    69  		return
    70  	}
    71  	c.Candles = append(c.Candles, *candle)
    72  	c.cursor = sz // len(c.Candles) - 1
    73  }
    74  
    75  func (c *Cache) Reset() {
    76  	c.cursor = 0
    77  	c.Candles = nil
    78  }
    79  
    80  // WireCandles encodes up to 'count' most recent candles as
    81  // *msgjson.WireCandles. If the Cache contains fewer than 'count', only those
    82  // available will be returned, with no indication of error.
    83  func (c *Cache) WireCandles(count int) *msgjson.WireCandles {
    84  	n := count
    85  	sz := len(c.Candles)
    86  	if sz < n {
    87  		n = sz
    88  	}
    89  	wc := msgjson.NewWireCandles(n)
    90  	for i := sz - n; i < sz; i++ {
    91  		candle := &c.Candles[(c.cursor+1+i)%sz]
    92  		wc.StartStamps = append(wc.StartStamps, candle.StartStamp)
    93  		wc.EndStamps = append(wc.EndStamps, candle.EndStamp)
    94  		wc.MatchVolumes = append(wc.MatchVolumes, candle.MatchVolume)
    95  		wc.QuoteVolumes = append(wc.QuoteVolumes, candle.QuoteVolume)
    96  		wc.HighRates = append(wc.HighRates, candle.HighRate)
    97  		wc.LowRates = append(wc.LowRates, candle.LowRate)
    98  		wc.StartRates = append(wc.StartRates, candle.StartRate)
    99  		wc.EndRates = append(wc.EndRates, candle.EndRate)
   100  	}
   101  
   102  	return wc
   103  }
   104  
   105  // CandlesCopy returns a deep copy of Candles with the oldest candle at the
   106  // first index.
   107  func (c *Cache) CandlesCopy() []msgjson.Candle {
   108  	sz := len(c.Candles)
   109  	candles := make([]msgjson.Candle, sz)
   110  	switch {
   111  	case sz == 0:
   112  	case sz == c.cap: // circular mode
   113  		oldIdx := (c.cursor + 1) % c.cap
   114  		copy(candles, c.Candles[oldIdx:])
   115  		copy(candles[sz-oldIdx:], c.Candles)
   116  	default:
   117  		copy(candles, c.Candles)
   118  	}
   119  	return candles
   120  }
   121  
   122  // Delta calculates the change in rate, as a percentage, and total volume over
   123  // the specified period going backwards from now. Because the first candle does
   124  // not necessarily align with the cutoff, the rate and volume contribution from
   125  // that candle is linearly interpreted between the endpoints. The caller is
   126  // responsible for making sure that dur >> binSize, otherwise the results will
   127  // be of little value.
   128  func (c *Cache) Delta(since time.Time) (changePct float64, vol, high, low uint64) {
   129  	cutoff := uint64(since.UnixMilli())
   130  	sz := len(c.Candles)
   131  	if sz == 0 {
   132  		return 0, 0, 0, 0
   133  	}
   134  	var startRate, endRate uint64
   135  	for i := 0; i < sz; i++ {
   136  		candle := &c.Candles[(c.cursor+sz-i)%sz]
   137  		if candle.EndStamp <= cutoff {
   138  			break
   139  		}
   140  
   141  		if endRate == 0 {
   142  			endRate = candle.EndRate
   143  			if endRate == 0 {
   144  				endRate = candle.StartRate
   145  			}
   146  		}
   147  
   148  		if low == 0 || (candle.LowRate > 0 && candle.LowRate < low) {
   149  			low = candle.LowRate
   150  		}
   151  		if candle.HighRate > high {
   152  			high = candle.HighRate
   153  		}
   154  
   155  		if candle.StartStamp <= cutoff && candle.StartRate != 0 {
   156  			// Interpret the point linearly between the start and end stamps
   157  			cut := float64(cutoff-candle.StartStamp) / float64(candle.EndStamp-candle.StartStamp)
   158  			rateDelta := float64(candle.EndRate) - float64(candle.StartRate)
   159  			r := candle.StartRate + uint64(cut*rateDelta)
   160  			if r > 0 {
   161  				startRate = r
   162  			}
   163  			vol += uint64((1 - cut) * float64(candle.MatchVolume))
   164  
   165  			break
   166  		} else if candle.StartRate != 0 {
   167  			startRate = candle.StartRate
   168  		} else if candle.EndRate != 0 {
   169  			startRate = candle.EndRate
   170  		}
   171  
   172  		vol += candle.MatchVolume
   173  	}
   174  	if startRate == 0 {
   175  		return 0, vol, high, low
   176  	}
   177  	return (float64(endRate) - float64(startRate)) / float64(startRate), vol, high, low
   178  }
   179  
   180  // Last gets the most recent candle in the cache.
   181  func (c *Cache) Last() *Candle {
   182  	return &c.Candles[c.cursor]
   183  }
   184  
   185  // CompletedCandlesSince returns any candles that fall into an epoch after the
   186  // epoch of the provided timestamp, and before the current epoch.
   187  func (c *Cache) CompletedCandlesSince(lastStoredEndStamp uint64) (cs []*Candle) {
   188  	currentIdx := uint64(time.Now().UnixMilli()) / c.BinSize
   189  	lastStoredIdx := lastStoredEndStamp / c.BinSize
   190  
   191  	sz := len(c.Candles)
   192  	for i := 0; i < sz; i++ {
   193  		// iterate backwards
   194  		candle := &c.Candles[(c.cursor+sz-i)%sz]
   195  		epochIdx := candle.EndStamp / c.BinSize
   196  		if epochIdx >= currentIdx {
   197  			continue
   198  		}
   199  		if epochIdx <= lastStoredIdx {
   200  			break
   201  		}
   202  		cs = append(cs, candle)
   203  	}
   204  	utils.ReverseSlice(cs)
   205  	return
   206  }
   207  
   208  // combineCandles attempts to add the candidate candle to the target candle
   209  // in-place, if they're in the same bin, otherwise returns false.
   210  func (c *Cache) combineCandles(target, candidate *Candle) bool {
   211  	if target.EndStamp/c.BinSize != candidate.EndStamp/c.BinSize {
   212  		// The candidate candle cannot be added.
   213  		return false
   214  	}
   215  	target.EndStamp = candidate.EndStamp
   216  	target.EndRate = candidate.EndRate
   217  	if candidate.HighRate > target.HighRate {
   218  		target.HighRate = candidate.HighRate
   219  	}
   220  	if candidate.LowRate < target.LowRate || target.LowRate == 0 {
   221  		target.LowRate = candidate.LowRate
   222  	}
   223  	target.MatchVolume += candidate.MatchVolume
   224  	target.QuoteVolume += candidate.QuoteVolume
   225  	return true
   226  }