package tradesignal import ( "strings" "sync" ) type mode int const ( modeNone mode = iota modeQty modeWeight ) type Snapshot struct { AvgPrice float64 Size float64 HasAvg bool } type state struct { avg float64 size float64 mode mode } type Tracker struct { mu sync.Mutex positions map[string]*state applied map[string]Snapshot } func NewTracker() *Tracker { return &Tracker{ positions: make(map[string]*state), applied: make(map[string]Snapshot), } } func (t *Tracker) Apply(signal *Signal) Snapshot { if signal == nil { return Snapshot{} } t.mu.Lock() defer t.mu.Unlock() if signal.SignalID != "" { if snap, ok := t.applied[signal.SignalID]; ok { return snap } } key := positionKey(signal.StrategyCode, signal.Symbol, signal.Side) action := strings.ToUpper(signal.Action) st := t.positions[key] var snap Snapshot switch action { case "OPEN": st = openPosition(signal) snap = snapshotFrom(st) if st != nil { t.positions[key] = st } else { delete(t.positions, key) } 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 } case "CLOSE": snap = snapshotFrom(st) delete(t.positions, key) default: snap = snapshotFrom(st) } if signal.SignalID != "" { t.applied[signal.SignalID] = snap } return snap } 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) }