package tradesignal import ( "context" "errors" "math" "sync" "testing" "aiaa-notification-service/internal/cache" ) type fakeKV struct { mu sync.Mutex data map[string][]byte fail bool } func newFakeKV() *fakeKV { return &fakeKV{data: make(map[string][]byte)} } func (f *fakeKV) GetRaw(_ context.Context, key string) ([]byte, error) { if f.fail { return nil, errors.New("redis down") } f.mu.Lock() defer f.mu.Unlock() if b, ok := f.data[key]; ok { return append([]byte(nil), b...), nil } return nil, nil } func (f *fakeKV) TxWrite(_ context.Context, writes []cache.KVWrite) error { if f.fail { return errors.New("redis down") } f.mu.Lock() defer f.mu.Unlock() for _, w := range writes { if w.Delete { delete(f.data, w.Key) continue } f.data[w.Key] = append([]byte(nil), w.Val...) } return nil } func TestTrackerPersistsAcrossMemoryInstances(t *testing.T) { store := newMemoryStore() mustApply(t, NewTrackerWithStore(store), &Signal{ SignalID: "p1", StrategyCode: "BLONG", Symbol: "PEPEUSDT", Side: "SHORT", Action: "OPEN", Quantity: ptr(10), Price: 0.00000059, }) snap := mustApply(t, NewTrackerWithStore(store), &Signal{ SignalID: "p2", StrategyCode: "BLONG", Symbol: "PEPEUSDT", Side: "SHORT", Action: "ADD", Quantity: ptr(10), Price: 0.00000061, }) if math.Abs(snap.AvgPrice-0.0000006) > 1e-12 { t.Fatalf("shared memory store avg=%v", snap.AvgPrice) } } func TestTrackerPersistsAcrossRedisInstances(t *testing.T) { store := &redisStore{c: newFakeKV()} mustApply(t, NewTrackerWithStore(store), &Signal{ SignalID: "r1", StrategyCode: "BLONG", Symbol: "PEPEUSDT", Side: "SHORT", Action: "OPEN", Quantity: ptr(10), Price: 100, }) snap := mustApply(t, NewTrackerWithStore(store), &Signal{ SignalID: "r2", StrategyCode: "BLONG", Symbol: "PEPEUSDT", Side: "SHORT", Action: "ADD", Quantity: ptr(10), Price: 200, }) if math.Abs(snap.AvgPrice-150) > 1e-9 { t.Fatalf("shared redis store avg=%v", snap.AvgPrice) } } func TestTrackerRedisIdempotentAcrossInstances(t *testing.T) { store := &redisStore{c: newFakeKV()} open := &Signal{ SignalID: "same", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "OPEN", Quantity: ptr(1), Price: 100, } mustApply(t, NewTrackerWithStore(store), open) mustApply(t, NewTrackerWithStore(store), open) snap := mustApply(t, NewTrackerWithStore(store), &Signal{ SignalID: "add", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "ADD", Quantity: ptr(1), Price: 200, }) if math.Abs(snap.AvgPrice-150) > 1e-9 { t.Fatalf("replayed open should not double size, avg=%v", snap.AvgPrice) } } func TestTrackerRedisStoreError(t *testing.T) { tr := NewTrackerWithStore(&redisStore{c: &fakeKV{fail: true, data: map[string][]byte{}}}) _, err := tr.Apply(&Signal{ SignalID: "e1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "OPEN", Quantity: ptr(1), Price: 100, }) if err == nil { t.Fatal("expected store error") } } func TestConvertPositionStoreError(t *testing.T) { c := &Converter{ positions: NewTrackerWithStore(&redisStore{c: &fakeKV{fail: true, data: map[string][]byte{}}}), } _, _, err := c.Convert([]byte(`{"action":"OPEN","symbol":"BTCUSDT","price":1,"quantity":1}`)) if !errors.Is(err, ErrPositionStore) { t.Fatalf("err=%v", err) } }