mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
The matching engine (pkg/lx, 45.5K LOC incl. tests) and the deterministic core layered on it (pkg/dex) are one matcher spread across two packages (dex imported lx in 12 files; lx imported neither). Fold lx into dex and drop the "lx" name entirely — pkg/dex is now the single matcher package. Pure rename/fold, ZERO logic change. Behavior proven identical: the merged pkg/dex runs 457 top-level tests (431 former-lx + 26 former-dex) and 489 test funcs (468 + 21), all green — matching, batch (BatchMatchEqualsSequential, run-to-run determinism, tile boundary), GPU/CPU parity, truncation, and CL swap math all pass unchanged. What moved: - git mv all 101 pkg/lx/*.go + 1 .c into pkg/dex/ (package lx -> package dex). - lx's swap_test.go -> swap_math_test.go (only filename collision with dex's existing swap_test.go). - Rewrote imports repo-wide: 54 pkg/lx import lines removed; 42 external importers (api, client, consensus, dchain, dpdk, lxgpu, marketdata, test/, tests/) repointed to pkg/dex with lx.X -> dex.X; the 12 in-package dex files had lx.X -> bare X. 717 selector lines rewritten. Stale pkg/lx comment refs updated (pkg/lxgpu left intact). Collisions resolved (2, both package-level vars): - ErrNoLiquidity: lx's pool-level error (dexError, used only in swap.go, never referenced via qualifier) renamed ErrPoolNoLiquidity to match the existing ErrPool* convention; dex's router-level ErrNoLiquidity (public, has errors.Is tests) kept. - priceMultiplierBig: identical after fold (both big.NewInt(PriceMultiplier)); deduped by deleting dex's copy. Not folded — pkg/orderbook: it is NOT the GPU seam (that is orderbook_gpu.go, which lives in the engine and folded in here). pkg/orderbook is a zero-importer, placeholder-cgo island whose 13 core domain identifiers (Order, OrderBook, Trade, OrderType, OrderStatus, PriceLevel, Buy, Sell, Market, Limit, Filled, PartiallyFilled, NewOrderBook) collide incompatibly with the engine's. Folding it verbatim breaks the build; folding via rename reintroduces the exact parallel duplication this fold removes. Left for owner decision (recommend deletion as dead code) — see refactor notes. Follow-on (separate repos, not touched): 19 files under luxfi/dex import pkg/lx externally — lux/precompile/dex, lux/precompile-dex/dex, lux/consensus/examples/01-simple-bridge, lux/_consensus-v12537/examples, lux/cex/pkg/engine — repoint to pkg/dex after this lands. Verified: CGO_ENABLED=0 GOWORK=off go build ./... clean; go vet ./... clean; go test ./... -count=1 all green (22 ok / 0 fail). Co-authored-by: Hanzo Dev <[email protected]>
709 lines
16 KiB
Go
709 lines
16 KiB
Go
package marketdata
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/dex/pkg/dex"
|
|
"github.com/luxfi/log"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIntervalDuration(t *testing.T) {
|
|
tests := []struct {
|
|
interval Interval
|
|
expected time.Duration
|
|
}{
|
|
{Interval1s, 1 * time.Second},
|
|
{Interval5s, 5 * time.Second},
|
|
{Interval15s, 15 * time.Second},
|
|
{Interval30s, 30 * time.Second},
|
|
{Interval1m, 1 * time.Minute},
|
|
{Interval3m, 3 * time.Minute},
|
|
{Interval5m, 5 * time.Minute},
|
|
{Interval15m, 15 * time.Minute},
|
|
{Interval30m, 30 * time.Minute},
|
|
{Interval1h, 1 * time.Hour},
|
|
{Interval2h, 2 * time.Hour},
|
|
{Interval4h, 4 * time.Hour},
|
|
{Interval6h, 6 * time.Hour},
|
|
{Interval8h, 8 * time.Hour},
|
|
{Interval12h, 12 * time.Hour},
|
|
{Interval1d, 24 * time.Hour},
|
|
{Interval3d, 3 * 24 * time.Hour},
|
|
{Interval1w, 7 * 24 * time.Hour},
|
|
{Interval1M, 30 * 24 * time.Hour},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(string(tt.interval), func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, tt.interval.Duration())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntervalDurationUnknown(t *testing.T) {
|
|
unknown := Interval("unknown")
|
|
// Unknown intervals default to 1 minute
|
|
assert.Equal(t, 1*time.Minute, unknown.Duration())
|
|
}
|
|
|
|
func TestAllIntervals(t *testing.T) {
|
|
intervals := AllIntervals()
|
|
|
|
assert.Len(t, intervals, 19)
|
|
assert.Contains(t, intervals, Interval1s)
|
|
assert.Contains(t, intervals, Interval1m)
|
|
assert.Contains(t, intervals, Interval1h)
|
|
assert.Contains(t, intervals, Interval1d)
|
|
assert.Contains(t, intervals, Interval1M)
|
|
}
|
|
|
|
func TestCandleStruct(t *testing.T) {
|
|
now := time.Now()
|
|
candle := Candle{
|
|
Symbol: "BTC/USDC",
|
|
Interval: Interval1h,
|
|
OpenTime: now,
|
|
CloseTime: now.Add(1 * time.Hour),
|
|
Open: 50000.0,
|
|
High: 51000.0,
|
|
Low: 49500.0,
|
|
Close: 50500.0,
|
|
Volume: 100.0,
|
|
Trades: 500,
|
|
Complete: true,
|
|
}
|
|
|
|
assert.Equal(t, "BTC/USDC", candle.Symbol)
|
|
assert.Equal(t, Interval1h, candle.Interval)
|
|
assert.Equal(t, 50000.0, candle.Open)
|
|
assert.Equal(t, 51000.0, candle.High)
|
|
assert.Equal(t, 49500.0, candle.Low)
|
|
assert.Equal(t, 50500.0, candle.Close)
|
|
assert.Equal(t, 100.0, candle.Volume)
|
|
assert.Equal(t, 500, candle.Trades)
|
|
assert.True(t, candle.Complete)
|
|
}
|
|
|
|
func TestCandleOHLCVRelationship(t *testing.T) {
|
|
candle := Candle{
|
|
Open: 100.0,
|
|
High: 110.0,
|
|
Low: 95.0,
|
|
Close: 105.0,
|
|
}
|
|
|
|
// High should be >= Open, Close
|
|
assert.GreaterOrEqual(t, candle.High, candle.Open)
|
|
assert.GreaterOrEqual(t, candle.High, candle.Close)
|
|
|
|
// Low should be <= Open, Close
|
|
assert.LessOrEqual(t, candle.Low, candle.Open)
|
|
assert.LessOrEqual(t, candle.Low, candle.Close)
|
|
|
|
// High should be >= Low
|
|
assert.GreaterOrEqual(t, candle.High, candle.Low)
|
|
}
|
|
|
|
func TestIntervalConstants(t *testing.T) {
|
|
// Verify string values
|
|
assert.Equal(t, "1s", string(Interval1s))
|
|
assert.Equal(t, "5s", string(Interval5s))
|
|
assert.Equal(t, "1m", string(Interval1m))
|
|
assert.Equal(t, "1h", string(Interval1h))
|
|
assert.Equal(t, "1d", string(Interval1d))
|
|
assert.Equal(t, "1M", string(Interval1M))
|
|
}
|
|
|
|
func TestIntervalSorting(t *testing.T) {
|
|
intervals := AllIntervals()
|
|
|
|
// First should be smallest (1s)
|
|
assert.Equal(t, Interval1s, intervals[0])
|
|
|
|
// Last should be largest (1M)
|
|
assert.Equal(t, Interval1M, intervals[len(intervals)-1])
|
|
}
|
|
|
|
func TestCandleJSON(t *testing.T) {
|
|
candle := Candle{
|
|
Symbol: "ETH/USDC",
|
|
Interval: Interval15m,
|
|
Open: 3000.0,
|
|
High: 3100.0,
|
|
Low: 2950.0,
|
|
Close: 3050.0,
|
|
Volume: 500.0,
|
|
Trades: 100,
|
|
}
|
|
|
|
// Verify JSON tags exist (would fail to compile otherwise)
|
|
assert.Equal(t, "ETH/USDC", candle.Symbol)
|
|
assert.Equal(t, Interval15m, candle.Interval)
|
|
}
|
|
|
|
func TestCandleIncomplete(t *testing.T) {
|
|
candle := Candle{
|
|
Symbol: "SOL/USDC",
|
|
Complete: false,
|
|
}
|
|
|
|
assert.False(t, candle.Complete)
|
|
}
|
|
|
|
func TestIntervalConversion(t *testing.T) {
|
|
// Convert string to Interval
|
|
intervalStr := "5m"
|
|
interval := Interval(intervalStr)
|
|
assert.Equal(t, Interval5m, interval)
|
|
assert.Equal(t, 5*time.Minute, interval.Duration())
|
|
}
|
|
|
|
func TestAllIntervalsUnique(t *testing.T) {
|
|
intervals := AllIntervals()
|
|
seen := make(map[Interval]bool)
|
|
|
|
for _, interval := range intervals {
|
|
assert.False(t, seen[interval], "Duplicate interval: %s", interval)
|
|
seen[interval] = true
|
|
}
|
|
}
|
|
|
|
func TestIntervalDurationOrder(t *testing.T) {
|
|
intervals := AllIntervals()
|
|
|
|
for i := 1; i < len(intervals); i++ {
|
|
prev := intervals[i-1].Duration()
|
|
curr := intervals[i].Duration()
|
|
assert.LessOrEqual(t, prev, curr, "Intervals should be in ascending order")
|
|
}
|
|
}
|
|
|
|
func TestCandleTimeRange(t *testing.T) {
|
|
now := time.Now().Truncate(time.Hour)
|
|
candle := Candle{
|
|
Interval: Interval1h,
|
|
OpenTime: now,
|
|
CloseTime: now.Add(Interval1h.Duration()),
|
|
}
|
|
|
|
duration := candle.CloseTime.Sub(candle.OpenTime)
|
|
assert.Equal(t, Interval1h.Duration(), duration)
|
|
}
|
|
|
|
func BenchmarkIntervalDuration(b *testing.B) {
|
|
interval := Interval1h
|
|
for i := 0; i < b.N; i++ {
|
|
_ = interval.Duration()
|
|
}
|
|
}
|
|
|
|
func BenchmarkAllIntervals(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
_ = AllIntervals()
|
|
}
|
|
}
|
|
|
|
// ===== Aggregator Tests =====
|
|
|
|
func newTestAggregator() *Aggregator {
|
|
db := memdb.New()
|
|
logger := log.NoLog{}
|
|
return NewAggregator(logger, db)
|
|
}
|
|
|
|
func TestNewAggregator(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
require.NotNil(t, agg)
|
|
assert.NotNil(t, agg.candles)
|
|
assert.NotNil(t, agg.trades)
|
|
assert.NotNil(t, agg.subscribers)
|
|
assert.NotNil(t, agg.ctx)
|
|
assert.NotNil(t, agg.cancel)
|
|
}
|
|
|
|
func TestAggregatorStartStop(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
err := agg.Start()
|
|
require.NoError(t, err)
|
|
|
|
// Let it run briefly
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Stop should work cleanly
|
|
agg.Stop()
|
|
}
|
|
|
|
func TestAggregatorAddTrade(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
trade := &dex.Trade{
|
|
Price: 50000.0,
|
|
Size: 1.5,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
agg.AddTrade(trade)
|
|
|
|
// Check trade was added to buffer
|
|
agg.tradesMu.Lock()
|
|
defer agg.tradesMu.Unlock()
|
|
assert.Len(t, agg.trades, 1)
|
|
assert.Equal(t, uint64(1), agg.totalTrades)
|
|
}
|
|
|
|
func TestAggregatorAddMultipleTrades(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
trade := &dex.Trade{
|
|
Price: 50000.0 + float64(i*100),
|
|
Size: float64(i + 1),
|
|
Timestamp: time.Now(),
|
|
}
|
|
agg.AddTrade(trade)
|
|
}
|
|
|
|
agg.tradesMu.Lock()
|
|
defer agg.tradesMu.Unlock()
|
|
assert.Len(t, agg.trades, 10)
|
|
assert.Equal(t, uint64(10), agg.totalTrades)
|
|
}
|
|
|
|
func TestAggregatorProcessTradeBuffer(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Add a trade
|
|
trade := &dex.Trade{
|
|
Price: 50000.0,
|
|
Size: 2.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
agg.AddTrade(trade)
|
|
|
|
// Process the buffer
|
|
agg.processTradeBuffer()
|
|
|
|
// Buffer should be empty after processing
|
|
agg.tradesMu.Lock()
|
|
bufferLen := len(agg.trades)
|
|
agg.tradesMu.Unlock()
|
|
assert.Equal(t, 0, bufferLen)
|
|
}
|
|
|
|
func TestAggregatorProcessEmptyBuffer(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Should not panic on empty buffer
|
|
agg.processTradeBuffer()
|
|
}
|
|
|
|
func TestAggregatorUpdateCandles(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
trade := &dex.Trade{
|
|
Price: 50000.0,
|
|
Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
agg.updateCandles(trade)
|
|
|
|
// Should have created candles for all intervals
|
|
agg.candlesMu.RLock()
|
|
defer agg.candlesMu.RUnlock()
|
|
|
|
symbolCandles := agg.candles["BTC-USD"]
|
|
assert.NotNil(t, symbolCandles)
|
|
assert.Greater(t, len(symbolCandles), 0)
|
|
}
|
|
|
|
func TestAggregatorCandleHighLow(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Use a fixed timestamp aligned to 1-minute boundary so all trades fall in the same 1m candle
|
|
baseTime := time.Date(2025, 1, 1, 10, 30, 0, 0, time.UTC)
|
|
|
|
// First trade - sets open
|
|
trade1 := &dex.Trade{Price: 50000.0, Size: 1.0, Timestamp: baseTime}
|
|
agg.updateCandles(trade1)
|
|
|
|
// Second trade - higher price (still within same minute)
|
|
trade2 := &dex.Trade{Price: 51000.0, Size: 1.0, Timestamp: baseTime.Add(10 * time.Second)}
|
|
agg.updateCandles(trade2)
|
|
|
|
// Third trade - lower price (still within same minute)
|
|
trade3 := &dex.Trade{Price: 49000.0, Size: 1.0, Timestamp: baseTime.Add(20 * time.Second)}
|
|
agg.updateCandles(trade3)
|
|
|
|
// Check 1-minute candle (not 1-second, since trades span multiple seconds)
|
|
agg.candlesMu.RLock()
|
|
defer agg.candlesMu.RUnlock()
|
|
|
|
candle := agg.candles["BTC-USD"][Interval1m]
|
|
require.NotNil(t, candle)
|
|
|
|
assert.Equal(t, 50000.0, candle.Open)
|
|
assert.Equal(t, 51000.0, candle.High)
|
|
assert.Equal(t, 49000.0, candle.Low)
|
|
assert.Equal(t, 49000.0, candle.Close)
|
|
assert.Equal(t, 3.0, candle.Volume)
|
|
assert.Equal(t, 3, candle.Trades)
|
|
}
|
|
|
|
func TestAggregatorGetLatestCandle(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// No candles yet
|
|
candle := agg.GetLatestCandle("BTC-USD", Interval1m)
|
|
assert.Nil(t, candle)
|
|
|
|
// Add a trade to create candle
|
|
trade := &dex.Trade{Price: 50000.0, Size: 1.0, Timestamp: time.Now()}
|
|
agg.updateCandles(trade)
|
|
|
|
// Now should have a candle
|
|
candle = agg.GetLatestCandle("BTC-USD", Interval1m)
|
|
require.NotNil(t, candle)
|
|
assert.Equal(t, "BTC-USD", candle.Symbol)
|
|
assert.Equal(t, Interval1m, candle.Interval)
|
|
}
|
|
|
|
func TestAggregatorGetLatestCandleUnknownSymbol(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
candle := agg.GetLatestCandle("UNKNOWN/USD", Interval1h)
|
|
assert.Nil(t, candle)
|
|
}
|
|
|
|
func TestAggregatorGetStats(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
stats := agg.GetStats()
|
|
assert.NotNil(t, stats)
|
|
assert.Contains(t, stats, "total_trades")
|
|
assert.Contains(t, stats, "total_candles")
|
|
assert.Contains(t, stats, "symbols")
|
|
assert.Equal(t, uint64(0), stats["total_trades"])
|
|
}
|
|
|
|
func TestAggregatorGetStatsWithData(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Add some trades
|
|
for i := 0; i < 5; i++ {
|
|
trade := &dex.Trade{Price: 50000.0 + float64(i), Size: 1.0, Timestamp: time.Now()}
|
|
agg.AddTrade(trade)
|
|
}
|
|
agg.processTradeBuffer()
|
|
|
|
stats := agg.GetStats()
|
|
assert.Equal(t, uint64(5), stats["total_trades"])
|
|
assert.Greater(t, stats["total_candles"], uint64(0))
|
|
}
|
|
|
|
func TestAggregatorSubscribe(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
ch := agg.Subscribe("BTC-USD", Interval1m)
|
|
require.NotNil(t, ch)
|
|
|
|
// Check subscriber was added
|
|
agg.subMu.RLock()
|
|
subs := agg.subscribers["BTC-USD:1m"]
|
|
agg.subMu.RUnlock()
|
|
assert.Len(t, subs, 1)
|
|
}
|
|
|
|
func TestAggregatorMultipleSubscribers(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
ch1 := agg.Subscribe("BTC-USD", Interval1m)
|
|
ch2 := agg.Subscribe("BTC-USD", Interval1m)
|
|
ch3 := agg.Subscribe("ETH-USD", Interval1m)
|
|
|
|
require.NotNil(t, ch1)
|
|
require.NotNil(t, ch2)
|
|
require.NotNil(t, ch3)
|
|
|
|
agg.subMu.RLock()
|
|
btcSubs := agg.subscribers["BTC-USD:1m"]
|
|
ethSubs := agg.subscribers["ETH-USD:1m"]
|
|
agg.subMu.RUnlock()
|
|
|
|
assert.Len(t, btcSubs, 2)
|
|
assert.Len(t, ethSubs, 1)
|
|
}
|
|
|
|
func TestAggregatorGetCandles(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
candles, err := agg.GetCandles("BTC-USD", Interval1m, 100)
|
|
require.NoError(t, err)
|
|
// Returns empty since iterator is not fully implemented
|
|
assert.NotNil(t, candles)
|
|
}
|
|
|
|
func TestAggregatorVWAP(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
vwap := agg.VolumeWeightedAveragePrice("BTC-USD", Interval1m, 10)
|
|
// Returns 0 since no candles
|
|
assert.Equal(t, 0.0, vwap)
|
|
}
|
|
|
|
func TestAggregatorMovingAverage(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
ma := agg.MovingAverage("BTC-USD", Interval1m, 10)
|
|
// Returns 0 since no candles
|
|
assert.Equal(t, 0.0, ma)
|
|
}
|
|
|
|
func TestAggregatorRSI(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
rsi := agg.RSI("BTC-USD", Interval1m, 14)
|
|
// Returns 50 (neutral) since no candles
|
|
assert.Equal(t, 50.0, rsi)
|
|
}
|
|
|
|
func TestAggregatorGetCandleOpenTime(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
tests := []struct {
|
|
name string
|
|
t time.Time
|
|
interval Interval
|
|
}{
|
|
{"1m at minute boundary", time.Date(2025, 1, 1, 10, 30, 0, 0, time.UTC), Interval1m},
|
|
{"1m mid-minute", time.Date(2025, 1, 1, 10, 30, 45, 0, time.UTC), Interval1m},
|
|
{"1h at hour boundary", time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC), Interval1h},
|
|
{"1d at day start", time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), Interval1d},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
openTime := agg.getCandleOpenTime(tt.t, tt.interval)
|
|
// Open time should be aligned to interval boundary
|
|
assert.True(t, openTime.Before(tt.t) || openTime.Equal(tt.t))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAggregatorGetCandleOpenTimeMonthly(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
testTime := time.Date(2025, 6, 15, 14, 30, 0, 0, time.UTC)
|
|
openTime := agg.getCandleOpenTime(testTime, Interval1M)
|
|
|
|
// Monthly should be first of the month
|
|
assert.Equal(t, 2025, openTime.Year())
|
|
assert.Equal(t, time.June, openTime.Month())
|
|
assert.Equal(t, 1, openTime.Day())
|
|
assert.Equal(t, 0, openTime.Hour())
|
|
assert.Equal(t, 0, openTime.Minute())
|
|
}
|
|
|
|
func TestAggregatorContextCancellation(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
err := agg.Start()
|
|
require.NoError(t, err)
|
|
|
|
// Cancel context should stop
|
|
agg.cancel()
|
|
// Wait a bit for goroutines to finish
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// wg.Wait() in Stop() should complete
|
|
done := make(chan struct{})
|
|
go func() {
|
|
agg.wg.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Good, goroutines stopped
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("Goroutines did not stop in time")
|
|
}
|
|
}
|
|
|
|
func TestAggregatorConcurrentTradeAdds(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
done := make(chan struct{})
|
|
for i := 0; i < 10; i++ {
|
|
go func(n int) {
|
|
for j := 0; j < 100; j++ {
|
|
trade := &dex.Trade{
|
|
Price: 50000.0 + float64(n*100+j),
|
|
Size: float64(j + 1),
|
|
Timestamp: time.Now(),
|
|
}
|
|
agg.AddTrade(trade)
|
|
}
|
|
done <- struct{}{}
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
|
|
// Should have 1000 trades
|
|
assert.Equal(t, uint64(1000), agg.totalTrades)
|
|
}
|
|
|
|
func TestAggregatorPublishCandle(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Subscribe first
|
|
ch := agg.Subscribe("BTC-USD", Interval1m)
|
|
|
|
// Create and publish a candle
|
|
candle := &Candle{
|
|
Symbol: "BTC-USD",
|
|
Interval: Interval1m,
|
|
Open: 50000.0,
|
|
High: 51000.0,
|
|
Low: 49000.0,
|
|
Close: 50500.0,
|
|
Volume: 100.0,
|
|
Trades: 50,
|
|
Complete: true,
|
|
}
|
|
|
|
agg.publishCandle(candle)
|
|
|
|
// Should receive the candle
|
|
select {
|
|
case received := <-ch:
|
|
assert.Equal(t, candle.Symbol, received.Symbol)
|
|
assert.Equal(t, candle.Close, received.Close)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("Did not receive candle")
|
|
}
|
|
}
|
|
|
|
func TestAggregatorPublishCandleNoSubscribers(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
candle := &Candle{
|
|
Symbol: "BTC-USD",
|
|
Interval: Interval1m,
|
|
Complete: true,
|
|
}
|
|
|
|
// Should not panic with no subscribers
|
|
agg.publishCandle(candle)
|
|
}
|
|
|
|
func TestAggregatorStoreCandle(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
candle := &Candle{
|
|
Symbol: "BTC-USD",
|
|
Interval: Interval1m,
|
|
OpenTime: time.Now(),
|
|
CloseTime: time.Now().Add(1 * time.Minute),
|
|
Open: 50000.0,
|
|
High: 51000.0,
|
|
Low: 49000.0,
|
|
Close: 50500.0,
|
|
Volume: 100.0,
|
|
Trades: 50,
|
|
Complete: true,
|
|
}
|
|
|
|
// Should not panic
|
|
agg.storeCandle(candle)
|
|
}
|
|
|
|
func TestAggregatorCompleteCandles(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Add a candle that should be completed
|
|
pastTime := time.Now().Add(-2 * time.Hour)
|
|
candle := &Candle{
|
|
Symbol: "BTC-USD",
|
|
Interval: Interval1h,
|
|
OpenTime: pastTime,
|
|
CloseTime: pastTime.Add(1 * time.Hour),
|
|
Open: 50000.0,
|
|
Close: 50500.0,
|
|
Complete: false,
|
|
}
|
|
|
|
agg.candlesMu.Lock()
|
|
agg.candles["BTC-USD"] = map[Interval]*Candle{
|
|
Interval1h: candle,
|
|
}
|
|
agg.candlesMu.Unlock()
|
|
|
|
// Complete candles should mark it as complete
|
|
agg.completeCandles(Interval1h)
|
|
|
|
agg.candlesMu.RLock()
|
|
// Candle should be deleted after completion
|
|
_, exists := agg.candles["BTC-USD"][Interval1h]
|
|
agg.candlesMu.RUnlock()
|
|
|
|
assert.False(t, exists, "Completed candle should be deleted from map")
|
|
}
|
|
|
|
func TestAggregatorCleanupOldCandles(t *testing.T) {
|
|
agg := newTestAggregator()
|
|
|
|
// Should not panic
|
|
agg.cleanupOldCandles()
|
|
}
|
|
|
|
func BenchmarkAggregatorAddTrade(b *testing.B) {
|
|
agg := newTestAggregator()
|
|
trade := &dex.Trade{
|
|
Price: 50000.0,
|
|
Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
agg.AddTrade(trade)
|
|
}
|
|
}
|
|
|
|
func BenchmarkAggregatorUpdateCandles(b *testing.B) {
|
|
agg := newTestAggregator()
|
|
trade := &dex.Trade{
|
|
Price: 50000.0,
|
|
Size: 1.0,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
agg.updateCandles(trade)
|
|
}
|
|
}
|
|
|
|
func BenchmarkAggregatorGetStats(b *testing.B) {
|
|
agg := newTestAggregator()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = agg.GetStats()
|
|
}
|
|
}
|
|
|
|
// Ensure unused imports are used
|
|
var _ = context.Background
|