diff --git a/logger/decision_logger.go b/logger/decision_logger.go index fc44de9b..c9630508 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -243,9 +243,11 @@ func (l *DecisionLogger) GetStatistics() (*Statistics, error) { switch action.Action { case "open_long", "open_short": stats.TotalOpenPositions++ - case "close_long", "close_short", "partial_close": + case "close_long", "close_short", "auto_close_long", "auto_close_short": stats.TotalClosePositions++ - // update_stop_loss 和 update_take_profit 不計入統計 + // 🔧 BUG FIX:partial_close 不計入 TotalClosePositions,避免重複計數 + // case "partial_close": // 不計數,因為只有完全平倉才算一次 + // update_stop_loss 和 update_take_profit 不計入統計 } } } @@ -349,9 +351,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna symbol := action.Symbol side := "" - if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" { + if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" || action.Action == "auto_close_long" { side = "long" - } else if action.Action == "open_short" || action.Action == "close_short" { + } else if action.Action == "open_short" || action.Action == "close_short" || action.Action == "auto_close_short" { side = "short" } @@ -377,10 +379,10 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna "quantity": action.Quantity, "leverage": action.Leverage, } - case "close_long", "close_short": + case "close_long", "close_short", "auto_close_long", "auto_close_short": // 移除已平仓记录 delete(openPositions, posKey) - // partial_close 不處理,保留持倉記錄 + // partial_close 不處理,保留持倉記錄 } } } @@ -395,9 +397,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna symbol := action.Symbol side := "" - if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" { + if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" || action.Action == "auto_close_long" { side = "long" - } else if action.Action == "open_short" || action.Action == "close_short" { + } else if action.Action == "open_short" || action.Action == "close_short" || action.Action == "auto_close_short" { side = "short" } @@ -418,14 +420,18 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna case "open_long", "open_short": // 更新开仓记录(可能已经在预填充时记录过了) openPositions[posKey] = map[string]interface{}{ - "side": side, - "openPrice": action.Price, - "openTime": action.Timestamp, - "quantity": action.Quantity, - "leverage": action.Leverage, + "side": side, + "openPrice": action.Price, + "openTime": action.Timestamp, + "quantity": action.Quantity, + "leverage": action.Leverage, + "remainingQuantity": action.Quantity, // 🔧 BUG FIX:追蹤剩餘數量 + "accumulatedPnL": 0.0, // 🔧 BUG FIX:累積部分平倉盈虧 + "partialCloseCount": 0, // 🔧 BUG FIX:部分平倉次數 + "partialCloseVolume": 0.0, // 🔧 BUG FIX:部分平倉總量 } - case "close_long", "close_short", "partial_close": + case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short": // 查找对应的开仓记录(可能来自预填充或当前窗口) if openPos, exists := openPositions[posKey]; exists { openPrice := openPos["openPrice"].(float64) @@ -434,15 +440,22 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) - // 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量 - actualQuantity := quantity + // 🔧 BUG FIX:取得追蹤字段(若不存在則初始化) + remainingQty, _ := openPos["remainingQuantity"].(float64) + if remainingQty == 0 { + remainingQty = quantity // 兼容舊數據(沒有 remainingQuantity 字段) + } + accumulatedPnL, _ := openPos["accumulatedPnL"].(float64) + partialCloseCount, _ := openPos["partialCloseCount"].(int) + partialCloseVolume, _ := openPos["partialCloseVolume"].(float64) + + // 对于 partial_close,使用实际平仓数量;否则使用剩余仓位数量 + actualQuantity := remainingQty if action.Action == "partial_close" { actualQuantity = action.Quantity } - // 计算实际盈亏(USDT) - // 合约交易 PnL 计算:actualQuantity × 价格差 - // 注意:杠杆不影响绝对盈亏,只影响保证金需求 + // 计算本次平仓的盈亏(USDT) var pnl float64 if side == "long" { pnl = actualQuantity * (action.Price - openPrice) @@ -450,61 +463,134 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna pnl = actualQuantity * (openPrice - action.Price) } - // 计算盈亏百分比(相对保证金) - positionValue := actualQuantity * openPrice - marginUsed := positionValue / float64(leverage) - pnlPct := 0.0 - if marginUsed > 0 { - pnlPct = (pnl / marginUsed) * 100 - } + // 🔧 BUG FIX:處理 partial_close 聚合邏輯 + if action.Action == "partial_close" { + // 累積盈虧和數量 + accumulatedPnL += pnl + remainingQty -= actualQuantity + partialCloseCount++ + partialCloseVolume += actualQuantity - // 记录交易结果 - outcome := TradeOutcome{ - Symbol: symbol, - Side: side, - Quantity: actualQuantity, - Leverage: leverage, - OpenPrice: openPrice, - ClosePrice: action.Price, - PositionValue: positionValue, - MarginUsed: marginUsed, - PnL: pnl, - PnLPct: pnlPct, - Duration: action.Timestamp.Sub(openTime).String(), - OpenTime: openTime, - CloseTime: action.Timestamp, - } + // 更新 openPositions(保留持倉記錄,但更新追蹤數據) + openPos["remainingQuantity"] = remainingQty + openPos["accumulatedPnL"] = accumulatedPnL + openPos["partialCloseCount"] = partialCloseCount + openPos["partialCloseVolume"] = partialCloseVolume - analysis.RecentTrades = append(analysis.RecentTrades, outcome) - analysis.TotalTrades++ + // 判斷是否已完全平倉 + if remainingQty <= 0.0001 { // 使用小閾值避免浮點誤差 + // ✅ 完全平倉:記錄為一筆完整交易 + positionValue := quantity * openPrice + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (accumulatedPnL / marginUsed) * 100 + } - // 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损) - if pnl > 0 { - analysis.WinningTrades++ - analysis.AvgWin += pnl - } else if pnl < 0 { - analysis.LosingTrades++ - analysis.AvgLoss += pnl - } - // pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数 + outcome := TradeOutcome{ + Symbol: symbol, + Side: side, + Quantity: quantity, // 使用原始總量 + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, // 最後一次平倉價格 + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: accumulatedPnL, // 🔧 使用累積盈虧 + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, + } - // 更新币种统计 - if _, exists := analysis.SymbolStats[symbol]; !exists { - analysis.SymbolStats[symbol] = &SymbolPerformance{ - Symbol: symbol, + analysis.RecentTrades = append(analysis.RecentTrades, outcome) + analysis.TotalTrades++ // 🔧 只在完全平倉時計數 + + // 分类交易 + if accumulatedPnL > 0 { + analysis.WinningTrades++ + analysis.AvgWin += accumulatedPnL + } else if accumulatedPnL < 0 { + analysis.LosingTrades++ + analysis.AvgLoss += accumulatedPnL + } + + // 更新币种统计 + if _, exists := analysis.SymbolStats[symbol]; !exists { + analysis.SymbolStats[symbol] = &SymbolPerformance{ + Symbol: symbol, + } + } + stats := analysis.SymbolStats[symbol] + stats.TotalTrades++ + stats.TotalPnL += accumulatedPnL + if accumulatedPnL > 0 { + stats.WinningTrades++ + } else if accumulatedPnL < 0 { + stats.LosingTrades++ + } + + // 刪除持倉記錄 + delete(openPositions, posKey) } - } - stats := analysis.SymbolStats[symbol] - stats.TotalTrades++ - stats.TotalPnL += pnl - if pnl > 0 { - stats.WinningTrades++ - } else if pnl < 0 { - stats.LosingTrades++ - } + // ⚠️ 否則不做任何操作(等待後續 partial_close 或 full close) - // 移除已平仓记录(partial_close 不刪除,因為還有剩餘倉位) - if action.Action != "partial_close" { + } else { + // 🔧 完全平倉(close_long/close_short/auto_close) + // 如果之前有部分平倉,需要加上累積的 PnL + totalPnL := accumulatedPnL + pnl + + positionValue := quantity * openPrice + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (totalPnL / marginUsed) * 100 + } + + outcome := TradeOutcome{ + Symbol: symbol, + Side: side, + Quantity: quantity, // 使用原始總量 + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: totalPnL, // 🔧 包含之前部分平倉的 PnL + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, + } + + analysis.RecentTrades = append(analysis.RecentTrades, outcome) + analysis.TotalTrades++ + + // 分类交易 + if totalPnL > 0 { + analysis.WinningTrades++ + analysis.AvgWin += totalPnL + } else if totalPnL < 0 { + analysis.LosingTrades++ + analysis.AvgLoss += totalPnL + } + + // 更新币种统计 + if _, exists := analysis.SymbolStats[symbol]; !exists { + analysis.SymbolStats[symbol] = &SymbolPerformance{ + Symbol: symbol, + } + } + stats := analysis.SymbolStats[symbol] + stats.TotalTrades++ + stats.TotalPnL += totalPnL + if totalPnL > 0 { + stats.WinningTrades++ + } else if totalPnL < 0 { + stats.LosingTrades++ + } + + // 刪除持倉記錄 delete(openPositions, posKey) } }