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 }