feat: subscribe to RabbitMQ trade signals and notify by rules
Consume configurable queues, format signals (including period), share NotifyService with HTTP, and drop duplicate bodies within 1h.
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user