feat(交易信号): 持仓状态持久化至 Redis 以保障重启与多实例下均价计算准确
Motivation: 持仓与已处理信号的状态此前仅存于进程内存,服务重启或多实例部署后会丢失,导致加仓均价、仓位大小等计算失真,重复信号也无法跨实例幂等。通过将状态持久化到 Redis,保证持仓跟踪跨重启、跨实例连续一致,提升通知内容的准确性与可靠性。 Changes: * 新增持仓存储抽象,支持内存与 Redis 两种实现,持仓状态与已处理信号快照按 TTL 持久化 * 持仓变更通过 Redis 事务管道原子提交,保证状态更新与幂等记录一致写入 * 存储写入失败时消息进入重试而非直接确认,避免状态丢失导致通知失真 * 缓存层新增原始值读取与批量事务写入能力,并在订阅器初始化时注入 Redis 依赖 * 补充跨实例持久化、幂等去重与存储失败场景的测试覆盖
This commit is contained in:
@@ -77,6 +77,15 @@ func TestApplyDoesNotChangeMarginRatioOnlySignals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -89,7 +98,7 @@ func TestAvgPriceOpenAndAdd(t *testing.T) {
|
||||
Quantity: ptr(2),
|
||||
Price: 100,
|
||||
}
|
||||
snap := tr.Apply(open)
|
||||
snap := mustApply(t, tr, open)
|
||||
if !snap.HasAvg || snap.AvgPrice != 100 {
|
||||
t.Fatalf("open avg=%v has=%v", snap.AvgPrice, snap.HasAvg)
|
||||
}
|
||||
@@ -103,7 +112,7 @@ func TestAvgPriceOpenAndAdd(t *testing.T) {
|
||||
Quantity: ptr(2),
|
||||
Price: 200,
|
||||
}
|
||||
snap = tr.Apply(add)
|
||||
snap = mustApply(t, tr, add)
|
||||
if !snap.HasAvg || math.Abs(snap.AvgPrice-150) > 1e-9 {
|
||||
t.Fatalf("expected avg 150, got %v", snap.AvgPrice)
|
||||
}
|
||||
@@ -121,7 +130,7 @@ func TestAvgPriceWithMarginRatio(t *testing.T) {
|
||||
AmountMarginRatio: ptr(0.1),
|
||||
Price: 100,
|
||||
}
|
||||
tr.Apply(open)
|
||||
mustApply(t, tr, open)
|
||||
|
||||
add := &Signal{
|
||||
SignalID: "r2",
|
||||
@@ -132,7 +141,7 @@ func TestAvgPriceWithMarginRatio(t *testing.T) {
|
||||
AmountMarginRatio: ptr(0.1),
|
||||
Price: 200,
|
||||
}
|
||||
snap := tr.Apply(add)
|
||||
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)
|
||||
}
|
||||
@@ -140,7 +149,7 @@ func TestAvgPriceWithMarginRatio(t *testing.T) {
|
||||
|
||||
func TestCloseKeepsEntryAvgInSnapshot(t *testing.T) {
|
||||
tr := NewTracker()
|
||||
tr.Apply(&Signal{
|
||||
mustApply(t, tr, &Signal{
|
||||
SignalID: "c1",
|
||||
StrategyCode: "BLONG",
|
||||
Symbol: "BTCUSDT",
|
||||
@@ -150,7 +159,7 @@ func TestCloseKeepsEntryAvgInSnapshot(t *testing.T) {
|
||||
Price: 64000,
|
||||
})
|
||||
|
||||
snap := tr.Apply(&Signal{
|
||||
snap := mustApply(t, tr, &Signal{
|
||||
SignalID: "c2",
|
||||
StrategyCode: "BLONG",
|
||||
Symbol: "BTCUSDT",
|
||||
@@ -162,7 +171,7 @@ func TestCloseKeepsEntryAvgInSnapshot(t *testing.T) {
|
||||
t.Fatalf("close should report entry avg 64000, got %v", snap.AvgPrice)
|
||||
}
|
||||
|
||||
snap = tr.Apply(&Signal{
|
||||
snap = mustApply(t, tr, &Signal{
|
||||
SignalID: "c3",
|
||||
StrategyCode: "BLONG",
|
||||
Symbol: "BTCUSDT",
|
||||
@@ -187,10 +196,10 @@ func TestSignalIDIdempotent(t *testing.T) {
|
||||
Quantity: ptr(1),
|
||||
Price: 100,
|
||||
}
|
||||
tr.Apply(sig)
|
||||
tr.Apply(sig)
|
||||
mustApply(t, tr, sig)
|
||||
mustApply(t, tr, sig)
|
||||
|
||||
snap := tr.Apply(&Signal{
|
||||
snap := mustApply(t, tr, &Signal{
|
||||
SignalID: "dup2",
|
||||
StrategyCode: "BLONG",
|
||||
Symbol: "BTCUSDT",
|
||||
@@ -206,11 +215,11 @@ func TestSignalIDIdempotent(t *testing.T) {
|
||||
|
||||
func TestDifferentSideIsolated(t *testing.T) {
|
||||
tr := NewTracker()
|
||||
tr.Apply(&Signal{
|
||||
mustApply(t, tr, &Signal{
|
||||
SignalID: "l1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "LONG",
|
||||
Action: "OPEN", Quantity: ptr(1), Price: 100,
|
||||
})
|
||||
snap := tr.Apply(&Signal{
|
||||
snap := mustApply(t, tr, &Signal{
|
||||
SignalID: "s1", StrategyCode: "BLONG", Symbol: "BTCUSDT", Side: "SHORT",
|
||||
Action: "OPEN", Quantity: ptr(1), Price: 200,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user