Fix: Resolve Trade History data loss and P&L calculation errors

Major fixes:
1. Trade History data loss issue
   - Root cause: Open records outside analysis window caused close matching failures
   - Solution: Pre-populate position state by reading 3x window of historical records
   - Ensures long-term positions (>5 hours) generate correct trade records
2. P&L calculation errors
   - Remove incorrect leverage multiplication from absolute P&L
   - Correct calculation: Futures P&L = quantity × price difference
   - Leverage only affects P&L percentage (relative to margin)
3. Other fixes
   - Break-even trades (pnl=0) no longer misclassified as losses
   - Perfect strategy shows Profit Factor as 999.0 instead of 0.0
   - Expand analysis window from 20 to 100 cycles (5 hours)
Files changed:
- logger/decision_logger.go: Core matching and calculation logic
- api/server.go: API analysis window
- trader/auto_trader.go: AI decision analysis window
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
tinkle-community
2025-10-30 17:58:25 +08:00
parent e249aaa16b
commit fd8b1477e7
3 changed files with 66 additions and 16 deletions
+3 -2
View File
@@ -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),
+60 -12
View File
@@ -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
}
}
+3 -2
View File
@@ -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以避免传递错误数据)