decred.org/dcrdex@v1.0.5/dex/candles/candles_test.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 "testing" 8 "time" 9 ) 10 11 const fiveMins = 5 * 60 * 1000 12 13 func TestCache(t *testing.T) { 14 // ctx, cancel := context.WithCancel(context.Background()) 15 // defer cancel() 16 const binSize = 10 17 const cacheCapacity = 5 18 cache := NewCache(cacheCapacity, binSize) 19 20 if cache.BinSize != binSize { 21 t.Fatalf("wrong bin size. wanted %d, got %d", binSize, cache.BinSize) 22 } 23 24 makeCandle := func(startStamp, endStamp, matchVol, quoteVol, bookVol, startRate, endRate, lowRate, highRate uint64) *Candle { 25 return &Candle{ 26 MatchVolume: matchVol, 27 QuoteVolume: quoteVol, 28 StartStamp: startStamp, 29 EndStamp: endStamp, 30 StartRate: startRate, 31 EndRate: endRate, 32 HighRate: highRate, 33 LowRate: lowRate, 34 } 35 } 36 37 checkCandleStamps := func(candle *Candle, startStamp, endStamp uint64) { 38 t.Helper() 39 if candle.StartStamp != startStamp { 40 t.Fatalf("wrong StartStamp. wanted %d, got %d", startStamp, candle.StartStamp) 41 } 42 if candle.EndStamp != endStamp { 43 t.Fatalf("wrong EndStamp. wanted %d, got %d", endStamp, candle.EndStamp) 44 } 45 } 46 47 checkCandleVolumes := func(candle *Candle, matchVol, quoteVol, bookVol uint64) { 48 t.Helper() 49 if candle.MatchVolume != matchVol { 50 t.Fatalf("wrong MatchVolume. wanted %d, got %d", matchVol, candle.MatchVolume) 51 } 52 if candle.QuoteVolume != quoteVol { 53 t.Fatalf("wrong QuoteVolume. wanted %d, got %d", quoteVol, candle.QuoteVolume) 54 } 55 } 56 57 checkCandleRates := func(candle *Candle, startRate, endRate, lowRate, highRate uint64) { 58 t.Helper() 59 if candle.StartRate != startRate { 60 t.Fatalf("wrong StartRate. wanted %d, got %d", startRate, candle.StartRate) 61 } 62 if candle.EndRate != endRate { 63 t.Fatalf("wrong EndRate. wanted %d, got %d", endRate, candle.EndRate) 64 } 65 if candle.LowRate != lowRate { 66 t.Fatalf("wrong LowRate. wanted %d, got %d", lowRate, candle.LowRate) 67 } 68 if candle.HighRate != highRate { 69 t.Fatalf("wrong HighRate. wanted %d, got %d", highRate, candle.HighRate) 70 } 71 } 72 73 // Check basic functionality. 74 cache.Add(makeCandle(11, 12, 100, 101, 100, 100, 100, 50, 150)) // start rate 100 75 if len(cache.Candles) != 1 { 76 t.Fatalf("Add didn't add") 77 } 78 lastCandle := cache.Last() 79 if lastCandle == nil { 80 t.Fatalf("failed to retrieve last candle") 81 } 82 checkCandleStamps(lastCandle, 11, 12) 83 checkCandleVolumes(lastCandle, 100, 101, 100) 84 checkCandleRates(lastCandle, 100, 100, 50, 150) 85 86 // A bunch of stamps from the same bin should not add any candles. 87 cache.Add(makeCandle(12, 13, 100, 101, 100, 100, 100, 25, 100)) // low rate 25 88 cache.Add(makeCandle(13, 14, 100, 101, 100, 100, 100, 50, 200)) // high rate 200 89 cache.Add(makeCandle(14, 15, 100, 101, 150, 100, 125, 50, 100)) // end book volume 150, end rate 125 90 if len(cache.Candles) != 1 { 91 t.Fatalf("Add didn't add") 92 } 93 checkCandleStamps(cache.Last(), 11, 15) 94 checkCandleVolumes(cache.Last(), 400, 404, 150) 95 checkCandleRates(cache.Last(), 100, 125, 25, 200) 96 97 // Two candles each in a new bin. 98 cache.Add(makeCandle(25, 27, 10, 11, 12, 13, 14, 15, 16)) 99 cache.Add(makeCandle(41, 48, 17, 18, 19, 20, 21, 22, 23)) 100 if len(cache.Candles) != 3 { 101 t.Fatalf("New candles didn't add") 102 } 103 checkCandleStamps(cache.Last(), 41, 48) 104 checkCandleVolumes(cache.Last(), 17, 18, 19) 105 checkCandleRates(cache.Last(), 20, 21, 22, 23) 106 107 // Candle combination is based on end stamp only. 108 cache.Add(makeCandle(49, 51, 24, 25, 26, 27, 28, 29, 30)) 109 if len(cache.Candles) != 4 { 110 t.Fatalf("straddling candle didn't create new entry") 111 } 112 113 // Adding two more should only increase length by 1, since capacity is 5. 114 cache.Add(makeCandle(61, 69, 24, 25, 26, 27, 28, 29, 30)) 115 cache.Add(makeCandle(71, 79, 54321, 25, 26, 27, 28, 29, 30)) 116 if len(cache.Candles) != cacheCapacity { 117 t.Fatalf("cache size not at capacity. wanted %d, found %d", cacheCapacity, len(cache.Candles)) 118 } 119 // The cache becomes circular, so the most recent will be at the previously 120 // oldest index, 0. 121 if cache.Last() != &cache.Candles[0] { 122 t.Fatalf("cache didn't wrap") 123 } 124 125 // Encoding should still put the most recent last. 126 wc := cache.WireCandles(5) 127 if len(wc.MatchVolumes) != 5 { 128 t.Fatalf("encoded %d wire candles, expected 5", len(wc.MatchVolumes)) 129 } 130 if wc.MatchVolumes[4] != 54321 { 131 t.Fatalf("encoding order incorrect") 132 } 133 134 // Same thing even if we request fewer. 135 wc = cache.WireCandles(1) 136 if wc.MatchVolumes[0] != 54321 { 137 t.Fatalf("single candle wasn't the last") 138 } 139 } 140 141 func TestDelta(t *testing.T) { 142 tNow := time.Now().Truncate(time.Millisecond) 143 now := uint64(tNow.UnixMilli()) 144 aDayAgo := now - 86400*1000 145 146 c := NewCache(5, fiveMins) 147 // This one shouldn't be included. 148 c.Add(&Candle{ 149 MatchVolume: 100, 150 StartStamp: aDayAgo - fiveMins, 151 EndStamp: aDayAgo, 152 StartRate: 100, 153 EndRate: 100, 154 }) 155 c.Add(&Candle{ 156 MatchVolume: 150, 157 StartStamp: aDayAgo, 158 EndStamp: aDayAgo + fiveMins, 159 StartRate: 100, 160 EndRate: 150, 161 }) 162 c.Add(&Candle{ 163 MatchVolume: 50, 164 StartStamp: now - fiveMins, 165 EndStamp: now, 166 StartRate: 125, 167 EndRate: 175, 168 }) 169 170 startCandle := &c.Candles[1] 171 172 testTime := tNow 173 check24 := func(expDelta float64, expVol uint64) { 174 t.Helper() 175 high, low := (expDelta + 0.01), (expDelta - 0.01) 176 delta24, vol24, _, _ := c.Delta(testTime.Add(-time.Hour * 24)) 177 if delta24 < low || delta24 > high { 178 t.Fatalf("wrong delta24. expected %.3f, got, %.3f", expDelta, delta24) 179 } 180 if vol24 != expVol { 181 t.Fatalf("wrong 24-hour volume. wanted %d, got %d", expVol, vol24) 182 } 183 } 184 185 // Basic function test. 186 check24(0.75, 200) 187 188 // Test halfway through the candle time. 125 -> 175 = +0.4, vol = 75 + 50 189 testTime = tNow.Add(time.Minute * 5 / 2) 190 check24(0.4, 125) 191 192 // Larger start rate tests underflow handling. 193 startCandle.StartRate, startCandle.EndRate = startCandle.EndRate, startCandle.StartRate 194 check24(0.4, 125) 195 startCandle.StartRate, startCandle.EndRate = startCandle.EndRate, startCandle.StartRate 196 197 testTime = tNow 198 199 // Zero-handling tests. 200 201 // A zero start rate on the first (used) candle should result in the EndRate 202 // being used as the base point instead. 125 -> 175 = 40% increase 203 startCandle.StartRate = 0 204 startCandle.EndRate = 125 205 check24(0.40, 200) 206 207 // A zero on the end rate too should result in that stick being skipped, but 208 // same result since start rate of next candle is same as end rate of this 209 // candle. 210 startCandle.EndRate = 0 211 check24(0.40, 200) 212 213 // Set that EndRate again, but delete the last candles EndRate, forcing use 214 // of the start rate instead. 100 -> 125 = 25% increase 215 startCandle.EndRate = 100 216 c.Candles[2].EndRate = 0 217 check24(0.25, 200) 218 219 } 220 221 func TestDeltaPartialDays(t *testing.T) { 222 tNow := time.Now().Truncate(time.Millisecond) 223 now := uint64(tNow.UnixMilli()) 224 aDayAgo := now - 86400*1000 225 226 c := NewCache(5, fiveMins) 227 c.Add(&Candle{ 228 MatchVolume: 444, 229 StartStamp: aDayAgo, 230 EndStamp: now, 231 StartRate: 50, 232 EndRate: 150, 233 }) 234 delta6, vol6, _, _ := c.Delta(tNow.Add(-time.Hour * 6)) 235 // In the last 6 hours, the rate would be interpreted as going from 125 to 236 // 150, change = 25/125 = 0.20 237 // Note that the cache would never be used with duration < binSize this way 238 // in practice. 239 if delta6 < 0.19 || delta6 > 0.21 { 240 t.Fatalf("wrong delta6. expected 0.25, got, %.3f", delta6) 241 } 242 if vol6 < 110 || vol6 > 111 { 243 t.Fatalf("wrong 12-hour volume. wanted 110, got %d", vol6) 244 } 245 } 246 247 func TestCandlesCopy(t *testing.T) { 248 smallCap := 10 249 binSize := uint64(60 * 5 * 1000) 250 cacheWithCandles := func(adds uint64) *Cache { 251 cache := NewCache(smallCap, binSize) 252 for i := uint64(0); i < adds; i++ { 253 candle := &Candle{ 254 StartStamp: i * binSize, 255 EndStamp: (i + 1) * binSize, 256 } 257 cache.Add(candle) 258 } 259 return cache 260 } 261 tests := []struct { 262 name string 263 adds uint64 264 wantLen int 265 wantFirst, wantLast uint64 // EndStamp times 266 }{{ 267 name: "no candles", 268 }, { 269 name: "one candle", 270 adds: 1, 271 wantLen: 1, 272 wantFirst: binSize, 273 wantLast: binSize, 274 }, { 275 name: "five candles", 276 adds: 5, 277 wantLen: 5, 278 wantFirst: binSize, 279 wantLast: binSize * 5, 280 }, { 281 name: "at cap before reusing zero", 282 adds: uint64(smallCap), 283 wantLen: smallCap, 284 wantFirst: binSize, 285 wantLast: binSize * 10, 286 }, { 287 name: "first candle over cap", 288 adds: 11, 289 wantLen: smallCap, 290 wantFirst: binSize * (11 + 1 - uint64(smallCap)), 291 wantLast: binSize * 11, 292 }, { 293 name: "many times over cap", 294 adds: 1345, 295 wantLen: smallCap, 296 wantFirst: binSize * (1345 + 1 - uint64(smallCap)), 297 wantLast: binSize * 1345, 298 }} 299 for _, test := range tests { 300 c := cacheWithCandles(test.adds) 301 cc := c.CandlesCopy() 302 if len(cc) != test.wantLen { 303 t.Fatalf("%q: wanted len %d but got %d", test.name, test.wantLen, len(cc)) 304 } 305 if test.wantLen != 0 { 306 first := cc[0].EndStamp 307 if first != test.wantFirst { 308 t.Fatalf("%q: wanted first end stamp %d but got %d", test.name, test.wantFirst, first) 309 } 310 last := cc[len(cc)-1].EndStamp 311 if last != test.wantLast { 312 t.Fatalf("%q: wanted last end stamp %d but got %d", test.name, test.wantLast, last) 313 } 314 } 315 } 316 }