From fd8b1477e78ebd7db9fe6c9da3b64a0782c9403f Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 30 Oct 2025 17:58:25 +0800 Subject: [PATCH] =?UTF-8?q?Fix:=20Resolve=20Trade=20History=20data=20loss?= =?UTF-8?q?=20and=20P&L=20calculation=20errors=20Major=20fixes:=201.=20Tra?= =?UTF-8?q?de=20History=20data=20loss=20issue=20=20=20=20-=20Root=20cause:?= =?UTF-8?q?=20Open=20records=20outside=20analysis=20window=20caused=20clos?= =?UTF-8?q?e=20matching=20failures=20=20=20=20-=20Solution:=20Pre-populate?= =?UTF-8?q?=20position=20state=20by=20reading=203x=20window=20of=20histori?= =?UTF-8?q?cal=20records=20=20=20=20-=20Ensures=20long-term=20positions=20?= =?UTF-8?q?(>5=20hours)=20generate=20correct=20trade=20records=202.=20P&L?= =?UTF-8?q?=20calculation=20errors=20=20=20=20-=20Remove=20incorrect=20lev?= =?UTF-8?q?erage=20multiplication=20from=20absolute=20P&L=20=20=20=20-=20C?= =?UTF-8?q?orrect=20calculation:=20Futures=20P&L=20=3D=20quantity=20=C3=97?= =?UTF-8?q?=20price=20difference=20=20=20=20-=20Leverage=20only=20affects?= =?UTF-8?q?=20P&L=20percentage=20(relative=20to=20margin)=203.=20Other=20f?= =?UTF-8?q?ixes=20=20=20=20-=20Break-even=20trades=20(pnl=3D0)=20no=20long?= =?UTF-8?q?er=20misclassified=20as=20losses=20=20=20=20-=20Perfect=20strat?= =?UTF-8?q?egy=20shows=20Profit=20Factor=20as=20999.0=20instead=20of=200.0?= =?UTF-8?q?=20=20=20=20-=20Expand=20analysis=20window=20from=2020=20to=201?= =?UTF-8?q?00=20cycles=20(5=20hours)=20Files=20changed:=20-=20logger/decis?= =?UTF-8?q?ion=5Flogger.go:=20Core=20matching=20and=20calculation=20logic?= =?UTF-8?q?=20-=20api/server.go:=20API=20analysis=20window=20-=20trader/au?= =?UTF-8?q?to=5Ftrader.go:=20AI=20decision=20analysis=20window=20Co-Author?= =?UTF-8?q?ed-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 5 +-- logger/decision_logger.go | 72 ++++++++++++++++++++++++++++++++------- trader/auto_trader.go | 5 +-- 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/api/server.go b/api/server.go index 875aeae0..ef871280 100644 --- a/api/server.go +++ b/api/server.go @@ -388,8 +388,9 @@ func (s *Server) handlePerformance(c *gin.Context) { return } - // 分析最近20个周期的交易表现 - performance, err := trader.GetDecisionLogger().AnalyzePerformance(20) + // 分析最近100个周期的交易表现(避免长期持仓的交易记录丢失) + // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易 + performance, err := trader.GetDecisionLogger().AnalyzePerformance(100) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("分析历史表现失败: %v", err), diff --git a/logger/decision_logger.go b/logger/decision_logger.go index e5acba8b..0338e1e2 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -330,7 +330,45 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna // 追踪持仓状态:symbol_side -> {side, openPrice, openTime, quantity, leverage} openPositions := make(map[string]map[string]interface{}) - // 遍历所有记录 + // 为了避免开仓记录在窗口外导致匹配失败,需要先从所有历史记录中找出未平仓的持仓 + // 获取更多历史记录来构建完整的持仓状态(使用更大的窗口) + allRecords, err := l.GetLatestRecords(lookbackCycles * 3) // 扩大3倍窗口 + if err == nil && len(allRecords) > len(records) { + // 先从扩大的窗口中收集所有开仓记录 + for _, record := range allRecords { + for _, action := range record.Decisions { + if !action.Success { + continue + } + + symbol := action.Symbol + side := "" + if action.Action == "open_long" || action.Action == "close_long" { + side = "long" + } else if action.Action == "open_short" || action.Action == "close_short" { + side = "short" + } + posKey := symbol + "_" + side + + switch action.Action { + case "open_long", "open_short": + // 记录开仓 + openPositions[posKey] = map[string]interface{}{ + "side": side, + "openPrice": action.Price, + "openTime": action.Timestamp, + "quantity": action.Quantity, + "leverage": action.Leverage, + } + case "close_long", "close_short": + // 移除已平仓记录 + delete(openPositions, posKey) + } + } + } + } + + // 遍历分析窗口内的记录,生成交易结果 for _, record := range records { for _, action := range record.Decisions { if !action.Success { @@ -348,7 +386,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna switch action.Action { case "open_long", "open_short": - // 记录开仓(包括数量和杠杆) + // 更新开仓记录(可能已经在预填充时记录过了) openPositions[posKey] = map[string]interface{}{ "side": side, "openPrice": action.Price, @@ -358,7 +396,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna } case "close_long", "close_short": - // 查找对应的开仓记录 + // 查找对应的开仓记录(可能来自预填充或当前窗口) if openPos, exists := openPositions[posKey]; exists { openPrice := openPos["openPrice"].(float64) openTime := openPos["openTime"].(time.Time) @@ -366,18 +404,23 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) - // 计算盈亏百分比 - pnlPct := 0.0 + // 计算实际盈亏(USDT) + // 合约交易 PnL 计算:quantity × 价格差 + // 注意:杠杆不影响绝对盈亏,只影响保证金需求 + var pnl float64 if side == "long" { - pnlPct = ((action.Price - openPrice) / openPrice) * 100 + pnl = quantity * (action.Price - openPrice) } else { - pnlPct = ((openPrice - action.Price) / openPrice) * 100 + pnl = quantity * (openPrice - action.Price) } - // 计算实际盈亏(USDT) - // PnL = 仓位价值 × 价格变化百分比 × 杠杆倍数 + // 计算盈亏百分比(相对保证金) positionValue := quantity * openPrice - pnl := positionValue * (pnlPct / 100) * float64(leverage) + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (pnl / marginUsed) * 100 + } // 记录交易结果 outcome := TradeOutcome{ @@ -395,13 +438,15 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna analysis.RecentTrades = append(analysis.RecentTrades, outcome) analysis.TotalTrades++ + // 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损) if pnl > 0 { analysis.WinningTrades++ analysis.AvgWin += pnl - } else { + } else if pnl < 0 { analysis.LosingTrades++ analysis.AvgLoss += pnl } + // pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数 // 更新币种统计 if _, exists := analysis.SymbolStats[symbol]; !exists { @@ -414,7 +459,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna stats.TotalPnL += pnl if pnl > 0 { stats.WinningTrades++ - } else { + } else if pnl < 0 { stats.LosingTrades++ } @@ -444,6 +489,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna // 注意:totalLossAmount 是负数,所以取负号得到绝对值 if totalLossAmount != 0 { analysis.ProfitFactor = totalWinAmount / (-totalLossAmount) + } else if totalWinAmount > 0 { + // 只有盈利没有亏损的情况,设置为一个很大的值表示完美策略 + analysis.ProfitFactor = 999.0 } } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 0d53ea3d..42bc2e69 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -510,8 +510,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { marginUsedPct = (totalMarginUsed / totalEquity) * 100 } - // 5. 分析历史表现(最近20个周期) - performance, err := at.decisionLogger.AnalyzePerformance(20) + // 5. 分析历史表现(最近100个周期,避免长期持仓的交易记录丢失) + // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易 + performance, err := at.decisionLogger.AnalyzePerformance(100) if err != nil { log.Printf("⚠️ 分析历史表现失败: %v", err) // 不影响主流程,继续执行(但设置performance为nil以避免传递错误数据)