fix: use symbol_side as peakPnLCache key to support dual-side positions (#657)

Fixes #652
Previously, peakPnLCache used only 'symbol' as the key, causing LONG
and SHORT positions of the same symbol to share the same peak P&L value.
This led to incorrect drawdown calculations and emergency close triggers.
Changes:
- checkPositionDrawdown: use posKey (symbol_side) for cache access
- UpdatePeakPnL: add side parameter and use posKey internally
- ClearPeakPnLCache: add side parameter and use posKey internally
Example fix:
- Before: peakPnLCache["BTCUSDT"] shared by both LONG and SHORT
- After: peakPnLCache["BTCUSDT_long"] and peakPnLCache["BTCUSDT_short"]
Impact:
- Fixes incorrect drawdown monitoring for dual positions
- Prevents false emergency close triggers on profitable positions
This commit is contained in:
Lawrence Liu
2025-11-08 10:34:01 +08:00
committed by GitHub
parent f73b4771b2
commit d23628a5a1

View File

@@ -1553,18 +1553,21 @@ func (at *AutoTrader) checkPositionDrawdown() {
currentPnLPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100 currentPnLPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100
} }
// 构造持仓唯一标识(区分多空)
posKey := symbol + "_" + side
// 获取该持仓的历史最高收益 // 获取该持仓的历史最高收益
at.peakPnLCacheMutex.RLock() at.peakPnLCacheMutex.RLock()
peakPnLPct, exists := at.peakPnLCache[symbol] peakPnLPct, exists := at.peakPnLCache[posKey]
at.peakPnLCacheMutex.RUnlock() at.peakPnLCacheMutex.RUnlock()
if !exists { if !exists {
// 如果没有历史最高记录,使用当前盈亏作为初始值 // 如果没有历史最高记录,使用当前盈亏作为初始值
peakPnLPct = currentPnLPct peakPnLPct = currentPnLPct
at.UpdatePeakPnL(symbol, currentPnLPct) at.UpdatePeakPnL(symbol, side, currentPnLPct)
} else { } else {
// 更新峰值缓存 // 更新峰值缓存
at.UpdatePeakPnL(symbol, currentPnLPct) at.UpdatePeakPnL(symbol, side, currentPnLPct)
} }
// 计算回撤(从最高点下跌的幅度) // 计算回撤(从最高点下跌的幅度)
@@ -1583,8 +1586,8 @@ func (at *AutoTrader) checkPositionDrawdown() {
log.Printf("❌ 回撤平仓失败 (%s %s): %v", symbol, side, err) log.Printf("❌ 回撤平仓失败 (%s %s): %v", symbol, side, err)
} else { } else {
log.Printf("✅ 回撤平仓成功: %s %s", symbol, side) log.Printf("✅ 回撤平仓成功: %s %s", symbol, side)
// 平仓后清理该symbol的缓存 // 平仓后清理该持仓的缓存
at.ClearPeakPnLCache(symbol) at.ClearPeakPnLCache(symbol, side)
} }
} else if currentPnLPct > 5.0 { } else if currentPnLPct > 5.0 {
// 记录接近平仓条件的情况(用于调试) // 记录接近平仓条件的情况(用于调试)
@@ -1630,25 +1633,27 @@ func (at *AutoTrader) GetPeakPnLCache() map[string]float64 {
} }
// UpdatePeakPnL 更新最高收益缓存 // UpdatePeakPnL 更新最高收益缓存
func (at *AutoTrader) UpdatePeakPnL(symbol string, currentPnLPct float64) { func (at *AutoTrader) UpdatePeakPnL(symbol, side string, currentPnLPct float64) {
at.peakPnLCacheMutex.Lock() at.peakPnLCacheMutex.Lock()
defer at.peakPnLCacheMutex.Unlock() defer at.peakPnLCacheMutex.Unlock()
if peak, exists := at.peakPnLCache[symbol]; exists { posKey := symbol + "_" + side
if peak, exists := at.peakPnLCache[posKey]; exists {
// 更新峰值如果是多头取较大值如果是空头currentPnLPct为负也要比较 // 更新峰值如果是多头取较大值如果是空头currentPnLPct为负也要比较
if currentPnLPct > peak { if currentPnLPct > peak {
at.peakPnLCache[symbol] = currentPnLPct at.peakPnLCache[posKey] = currentPnLPct
} }
} else { } else {
// 首次记录 // 首次记录
at.peakPnLCache[symbol] = currentPnLPct at.peakPnLCache[posKey] = currentPnLPct
} }
} }
// ClearPeakPnLCache 清除指定symbol的峰值缓存 // ClearPeakPnLCache 清除指定持仓的峰值缓存
func (at *AutoTrader) ClearPeakPnLCache(symbol string) { func (at *AutoTrader) ClearPeakPnLCache(symbol, side string) {
at.peakPnLCacheMutex.Lock() at.peakPnLCacheMutex.Lock()
defer at.peakPnLCacheMutex.Unlock() defer at.peakPnLCacheMutex.Unlock()
delete(at.peakPnLCache, symbol) posKey := symbol + "_" + side
delete(at.peakPnLCache, posKey)
} }