6f846a0a3c
Consume configurable queues, format signals (including period), share NotifyService with HTTP, and drop duplicate bodies within 1h.
221 lines
4.9 KiB
Go
221 lines
4.9 KiB
Go
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 TestAvgPriceOpenAndAdd(t *testing.T) {
|
|
tr := NewTracker()
|
|
|
|
open := &Signal{
|
|
SignalID: "s1",
|
|
StrategyCode: "BLONG",
|
|
Symbol: "BTCUSDT",
|
|
Side: "LONG",
|
|
Action: "OPEN",
|
|
Quantity: ptr(2),
|
|
Price: 100,
|
|
}
|
|
snap := tr.Apply(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 = tr.Apply(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,
|
|
}
|
|
tr.Apply(open)
|
|
|
|
add := &Signal{
|
|
SignalID: "r2",
|
|
StrategyCode: "BLONG",
|
|
Symbol: "ETHUSDT",
|
|
Side: "LONG",
|
|
Action: "ADD",
|
|
AmountMarginRatio: ptr(0.1),
|
|
Price: 200,
|
|
}
|
|
snap := tr.Apply(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()
|
|
tr.Apply(&Signal{
|
|
SignalID: "c1",
|
|
StrategyCode: "BLONG",
|
|
Symbol: "BTCUSDT",
|
|
Side: "LONG",
|
|
Action: "OPEN",
|
|
Quantity: ptr(1),
|
|
Price: 64000,
|
|
})
|
|
|
|
snap := tr.Apply(&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 = tr.Apply(&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,
|
|
}
|
|
tr.Apply(sig)
|
|
tr.Apply(sig)
|
|
|
|
snap := tr.Apply(&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()
|
|
tr.Apply(&Signal{
|
|
SignalID: "l1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG",
|
|
Action: "OPEN", Quantity: ptr(1), Price: 100,
|
|
})
|
|
snap := tr.Apply(&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)
|
|
}
|
|
}
|