package tradesignal import ( "math" "testing" "aiaa-notification-service/internal/config" ) func TestApplyQuantityMultiplierAndLeverage(t *testing.T) { leverage := 20 override := &config.StrategyOverride{ QuantityMultipliers: config.QuantityMultipliers{ Open: 2, Add: 1.5, Reduce: 0.5, Close: 3, }, Leverage: &leverage, } tests := []struct { action string quantity float64 wantQty float64 }{ {"OPEN", 1, 2}, {"ADD", 2, 3}, {"REDUCE", 4, 2}, {"CLOSE", 1, 3}, } for _, tt := range tests { signal := &Signal{ Action: tt.action, Quantity: ptr(tt.quantity), Leverage: 10, } out := Apply(signal, override) if out.Quantity == nil || *out.Quantity != tt.wantQty { t.Fatalf("action=%s quantity=%v want %v", tt.action, out.Quantity, tt.wantQty) } if out.Leverage != 20 { t.Fatalf("action=%s leverage=%d want 20", tt.action, out.Leverage) } } } func TestApplyKeepsOriginalWhenNoOverride(t *testing.T) { signal := &Signal{ Action: "OPEN", Quantity: ptr(1.5), Leverage: 8, } out := Apply(signal, nil) if out != signal { t.Fatalf("expected same signal pointer when override is nil") } } func TestApplyDoesNotChangeMarginRatioOnlySignals(t *testing.T) { marginRatio := 0.2 signal := &Signal{ Action: "OPEN", AmountMarginRatio: &marginRatio, Leverage: 5, } override := &config.StrategyOverride{ QuantityMultipliers: config.QuantityMultipliers{Open: 2}, } out := Apply(signal, override) if out.Quantity != nil { t.Fatalf("expected quantity unchanged when only margin ratio is set") } if out.Leverage != 5 { t.Fatalf("expected leverage unchanged, got %d", out.Leverage) } } func mustApply(t *testing.T, tr *Tracker, signal *Signal) Snapshot { t.Helper() snap, err := tr.Apply(signal) if err != nil { t.Fatal(err) } return snap } func TestAvgPriceOpenAndAdd(t *testing.T) { tr := NewTracker() open := &Signal{ SignalID: "s1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "OPEN", Quantity: ptr(2), Price: 100, } snap := mustApply(t, tr, open) if !snap.HasAvg || snap.AvgPrice != 100 { t.Fatalf("open avg=%v has=%v", snap.AvgPrice, snap.HasAvg) } add := &Signal{ SignalID: "s2", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "ADD", Quantity: ptr(2), Price: 200, } snap = mustApply(t, tr, add) if !snap.HasAvg || math.Abs(snap.AvgPrice-150) > 1e-9 { t.Fatalf("expected avg 150, got %v", snap.AvgPrice) } } func TestAvgPriceWithMarginRatio(t *testing.T) { tr := NewTracker() open := &Signal{ SignalID: "r1", StrategyCode: "BLONG", Symbol: "ETHUSDT", Side: "LONG", Action: "OPEN", AmountMarginRatio: ptr(0.1), Price: 100, } mustApply(t, tr, open) add := &Signal{ SignalID: "r2", StrategyCode: "BLONG", Symbol: "ETHUSDT", Side: "LONG", Action: "ADD", AmountMarginRatio: ptr(0.1), Price: 200, } snap := mustApply(t, tr, add) if !snap.HasAvg || math.Abs(snap.AvgPrice-150) > 1e-9 { t.Fatalf("expected weighted avg 150, got %v", snap.AvgPrice) } } func TestCloseKeepsEntryAvgInSnapshot(t *testing.T) { tr := NewTracker() mustApply(t, tr, &Signal{ SignalID: "c1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "OPEN", Quantity: ptr(1), Price: 64000, }) snap := mustApply(t, tr, &Signal{ SignalID: "c2", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "CLOSE", Price: 65000, }) if !snap.HasAvg || snap.AvgPrice != 64000 { t.Fatalf("close should report entry avg 64000, got %v", snap.AvgPrice) } snap = mustApply(t, tr, &Signal{ SignalID: "c3", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "ADD", Quantity: ptr(1), Price: 70000, }) if !snap.HasAvg || snap.AvgPrice != 70000 { t.Fatalf("after close, add should reopen at 70000, got %v", snap.AvgPrice) } } func TestSignalIDIdempotent(t *testing.T) { tr := NewTracker() sig := &Signal{ SignalID: "dup", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "OPEN", Quantity: ptr(1), Price: 100, } mustApply(t, tr, sig) mustApply(t, tr, sig) snap := mustApply(t, tr, &Signal{ SignalID: "dup2", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "ADD", Quantity: ptr(1), Price: 200, }) if math.Abs(snap.AvgPrice-150) > 1e-9 { t.Fatalf("duplicate open should not double size, avg=%v", snap.AvgPrice) } } func TestDifferentSideIsolated(t *testing.T) { tr := NewTracker() mustApply(t, tr, &Signal{ SignalID: "l1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG", Action: "OPEN", Quantity: ptr(1), Price: 100, }) snap := mustApply(t, tr, &Signal{ SignalID: "s1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "SHORT", Action: "OPEN", Quantity: ptr(1), Price: 200, }) if snap.AvgPrice != 200 { t.Fatalf("short should be isolated, got %v", snap.AvgPrice) } }