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  }