Files
aiaa-notification-server/internal/subscriber/tradesignal/position.go
T
ryan b396638438 feat(交易信号): 持仓状态持久化至 Redis 以保障重启与多实例下均价计算准确
Motivation:
持仓与已处理信号的状态此前仅存于进程内存,服务重启或多实例部署后会丢失,导致加仓均价、仓位大小等计算失真,重复信号也无法跨实例幂等。通过将状态持久化到 Redis,保证持仓跟踪跨重启、跨实例连续一致,提升通知内容的准确性与可靠性。

Changes:

* 新增持仓存储抽象,支持内存与 Redis 两种实现,持仓状态与已处理信号快照按 TTL 持久化
* 持仓变更通过 Redis 事务管道原子提交,保证状态更新与幂等记录一致写入
* 存储写入失败时消息进入重试而非直接确认,避免状态丢失导致通知失真
* 缓存层新增原始值读取与批量事务写入能力,并在订阅器初始化时注入 Redis 依赖
* 补充跨实例持久化、幂等去重与存储失败场景的测试覆盖
2026-08-23 14:43:53 +08:00

209 lines
3.8 KiB
Go

package tradesignal
import (
"strings"
"sync"
)
type mode int
const (
modeNone mode = iota
modeQty
modeWeight
)
type Snapshot struct {
AvgPrice float64 `json:"avgPrice"`
Size float64 `json:"size"`
HasAvg bool `json:"hasAvg"`
}
type state struct {
avg float64
size float64
mode mode
}
type Tracker struct {
mu sync.Mutex
store positionStore
}
func NewTracker() *Tracker {
return NewTrackerWithStore(newMemoryStore())
}
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{}, nil
}
t.mu.Lock()
defer t.mu.Unlock()
if signal.SignalID != "" {
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, 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 {
del = true
}
case "ADD":
st = addPosition(st, signal)
snap = snapshotFrom(st)
case "REDUCE":
snap = snapshotFrom(st)
st = reducePosition(st, signal)
if st == nil || st.size <= 0 {
del = true
st = nil
}
case "CLOSE":
snap = snapshotFrom(st)
del = true
st = nil
default:
snap = snapshotFrom(st)
}
if err := t.store.commit(key, st, del, signal.SignalID, snap); err != nil {
return Snapshot{}, err
}
return snap, nil
}
func openPosition(signal *Signal) *state {
if qty, ok := positiveQty(signal.Quantity); ok {
return &state{avg: signal.Price, size: qty, mode: modeQty}
}
if w, ok := positiveRatio(signal.AmountMarginRatio); ok {
return &state{avg: signal.Price, size: w, mode: modeWeight}
}
if signal.Price > 0 {
return &state{avg: signal.Price, size: 0, mode: modeNone}
}
return nil
}
func addPosition(st *state, signal *Signal) *state {
if st == nil || st.size <= 0 {
return openPosition(signal)
}
if qty, ok := positiveQty(signal.Quantity); ok {
if st.mode == modeWeight {
return st
}
if st.mode == modeNone || st.size == 0 {
st.mode = modeQty
st.size = qty
st.avg = signal.Price
return st
}
st.avg = (st.size*st.avg + qty*signal.Price) / (st.size + qty)
st.size += qty
st.mode = modeQty
return st
}
if w, ok := positiveRatio(signal.AmountMarginRatio); ok {
if st.mode == modeQty {
return st
}
if st.mode == modeNone || st.size == 0 {
st.mode = modeWeight
st.size = w
st.avg = signal.Price
return st
}
st.avg = (st.size*st.avg + w*signal.Price) / (st.size + w)
st.size += w
st.mode = modeWeight
return st
}
return st
}
func reducePosition(st *state, signal *Signal) *state {
if st == nil {
return nil
}
if qty, ok := positiveQty(signal.Quantity); ok && st.mode == modeQty {
st.size -= qty
if st.size < 0 {
st.size = 0
}
return st
}
ratio := 0.0
if r, ok := positiveRatio(signal.PosMarginRatio); ok {
ratio = r
} else if signal.Quantity == nil && signal.PosMarginRatio == nil {
return st
}
if ratio > 1 {
ratio = 1
}
if ratio > 0 {
st.size *= (1 - ratio)
}
return st
}
func snapshotFrom(st *state) Snapshot {
if st == nil || st.avg <= 0 {
return Snapshot{}
}
return Snapshot{
AvgPrice: st.avg,
Size: st.size,
HasAvg: true,
}
}
func positiveQty(q *float64) (float64, bool) {
if q == nil || *q <= 0 {
return 0, false
}
return *q, true
}
func positiveRatio(r *float64) (float64, bool) {
if r == nil || *r <= 0 {
return 0, false
}
return *r, true
}
func positionKey(strategyCode, symbol, side string) string {
return strings.ToUpper(strategyCode) + "|" + strings.ToUpper(symbol) + "|" + strings.ToUpper(side)
}