feat(交易信号): 持仓状态持久化至 Redis 以保障重启与多实例下均价计算准确
Motivation: 持仓与已处理信号的状态此前仅存于进程内存,服务重启或多实例部署后会丢失,导致加仓均价、仓位大小等计算失真,重复信号也无法跨实例幂等。通过将状态持久化到 Redis,保证持仓跟踪跨重启、跨实例连续一致,提升通知内容的准确性与可靠性。 Changes: * 新增持仓存储抽象,支持内存与 Redis 两种实现,持仓状态与已处理信号快照按 TTL 持久化 * 持仓变更通过 Redis 事务管道原子提交,保证状态更新与幂等记录一致写入 * 存储写入失败时消息进入重试而非直接确认,避免状态丢失导致通知失真 * 缓存层新增原始值读取与批量事务写入能力,并在订阅器初始化时注入 Redis 依赖 * 补充跨实例持久化、幂等去重与存储失败场景的测试覆盖
This commit is contained in:
@@ -14,9 +14,9 @@ const (
|
||||
)
|
||||
|
||||
type Snapshot struct {
|
||||
AvgPrice float64
|
||||
Size float64
|
||||
HasAvg bool
|
||||
AvgPrice float64 `json:"avgPrice"`
|
||||
Size float64 `json:"size"`
|
||||
HasAvg bool `json:"hasAvg"`
|
||||
}
|
||||
|
||||
type state struct {
|
||||
@@ -26,71 +26,75 @@ type state struct {
|
||||
}
|
||||
|
||||
type Tracker struct {
|
||||
mu sync.Mutex
|
||||
positions map[string]*state
|
||||
applied map[string]Snapshot
|
||||
mu sync.Mutex
|
||||
store positionStore
|
||||
}
|
||||
|
||||
func NewTracker() *Tracker {
|
||||
return &Tracker{
|
||||
positions: make(map[string]*state),
|
||||
applied: make(map[string]Snapshot),
|
||||
}
|
||||
return NewTrackerWithStore(newMemoryStore())
|
||||
}
|
||||
|
||||
func (t *Tracker) Apply(signal *Signal) Snapshot {
|
||||
func NewTrackerWithStore(store positionStore) *Tracker {
|
||||
if store == nil {
|
||||
store = newMemoryStore()
|
||||
}
|
||||
return &Tracker{store: store}
|
||||
}
|
||||
|
||||
func (t *Tracker) Apply(signal *Signal) (Snapshot, error) {
|
||||
if signal == nil {
|
||||
return Snapshot{}
|
||||
return Snapshot{}, nil
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if signal.SignalID != "" {
|
||||
if snap, ok := t.applied[signal.SignalID]; ok {
|
||||
return snap
|
||||
if snap, ok, err := t.store.loadApplied(signal.SignalID); err != nil {
|
||||
return Snapshot{}, err
|
||||
} else if ok {
|
||||
return snap, nil
|
||||
}
|
||||
}
|
||||
|
||||
key := positionKey(signal.StrategyCode, signal.Symbol, signal.Side)
|
||||
action := strings.ToUpper(signal.Action)
|
||||
st := t.positions[key]
|
||||
st, err := t.store.load(key)
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
|
||||
var snap Snapshot
|
||||
del := false
|
||||
switch action {
|
||||
case "OPEN":
|
||||
st = openPosition(signal)
|
||||
snap = snapshotFrom(st)
|
||||
if st != nil {
|
||||
t.positions[key] = st
|
||||
} else {
|
||||
delete(t.positions, key)
|
||||
if st == nil {
|
||||
del = true
|
||||
}
|
||||
case "ADD":
|
||||
st = addPosition(st, signal)
|
||||
snap = snapshotFrom(st)
|
||||
if st != nil {
|
||||
t.positions[key] = st
|
||||
}
|
||||
case "REDUCE":
|
||||
snap = snapshotFrom(st)
|
||||
st = reducePosition(st, signal)
|
||||
if st == nil || st.size <= 0 {
|
||||
delete(t.positions, key)
|
||||
} else {
|
||||
t.positions[key] = st
|
||||
del = true
|
||||
st = nil
|
||||
}
|
||||
case "CLOSE":
|
||||
snap = snapshotFrom(st)
|
||||
delete(t.positions, key)
|
||||
del = true
|
||||
st = nil
|
||||
default:
|
||||
snap = snapshotFrom(st)
|
||||
}
|
||||
|
||||
if signal.SignalID != "" {
|
||||
t.applied[signal.SignalID] = snap
|
||||
if err := t.store.commit(key, st, del, signal.SignalID, snap); err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
return snap
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
func openPosition(signal *Signal) *state {
|
||||
|
||||
Reference in New Issue
Block a user