mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-05 03:50:59 +08:00
Merge pull request #436 from zhouyongyou/fix/partial-close-stats
fix(ui): prevent system_prompt_template overwrite when value is empty string
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user