From d1e6c965cbcd6370527a683e5ff9324782c01135 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Mon, 4 May 2026 02:00:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20blacklistCoin=20=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=80=80=E5=87=BAPnL=E5=AD=97=E6=AE=B5,=20=E9=81=BF=E5=85=8DDB?= =?UTF-8?q?=E5=86=99=E5=85=A5=E9=9B=B6=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blacklistCoin 之前只设 Status=close_failed 就跑了, 没给 ExitDiffPct/ExitNetPnl/ExitLongPnl/ExitTotalFees 等字段赋值。 retryClose 写 TradeRecord 时全为零, 导致DB出现 PnL=0 的假记录。 修复: blacklistCoin 现在接收当前价格, 用 checkExit 相同方式 计算 PnL/价差收敛标签并存入 pos 字段。 --- trader.go | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/trader.go b/trader.go index 6d7e981..2fb11ee 100644 --- a/trader.go +++ b/trader.go @@ -256,7 +256,7 @@ func (t *Trader) Tick(store *PriceStore, notifier *Notifier) { // Blacklist: if position still open after 10 minutes without converging, // the spread is likely stale data. Add coin to blacklist and force close. if pos.Status == "open" && elapsed > 10*time.Minute { - t.blacklistCoin(pos, notifier) + t.blacklistCoin(pos, bgP, hlP, diffPct, notifier) } } } @@ -991,17 +991,52 @@ func (t *Trader) restoreOpenPositions() { } // blacklistCoin adds a coin to the blacklist and force-closes its position. -func (t *Trader) blacklistCoin(pos *ArbPosition, notifier *Notifier) { +// Calculates exit PnL fields so retryClose writes correct data to DB. +func (t *Trader) blacklistCoin(pos *ArbPosition, bgP, hlP, diffPct float64, notifier *Notifier) { + // Compute exit PnL the same way checkExit does + var longCurrent, shortCurrent float64 + if pos.LongLeg.Exchange == ExBitget { + longCurrent, shortCurrent = bgP, hlP + } else { + longCurrent, shortCurrent = hlP, bgP + } + longAvg := weightedAvgPrice(pos.LongEntryPrices, t.cfg.TradeAmountUSD) + shortAvg := weightedAvgPrice(pos.ShortEntryPrices, t.cfg.TradeAmountUSD) + longPnl := (longCurrent - longAvg) / longAvg * 100 + shortPnl := (shortAvg - shortCurrent) / shortAvg * 100 + totalFees := 2 * (takerFees[ExBitget] + takerFees[ExHyperLiquid]) + netPnl := longPnl + shortPnl - totalFees + + pos.ExitDiffPct = diffPct + pos.ExitNetPnl = netPnl + pos.ExitLongPnl = longPnl + pos.ExitShortPnl = shortPnl + pos.ExitTotalFees = totalFees + pos.LongLeg.ExitPrice = longCurrent + pos.ShortLeg.ExitPrice = shortCurrent + pos.ExitReasonText = "黑名单强平" + + // Convergence label + convergedPct := (pos.EntrySpread - diffPct) / pos.EntrySpread * 100 + if convergedPct < -10 { + pos.ExitConvergence = "价差发散" + } else if convergedPct < 10 { + pos.ExitConvergence = "价差持平" + } else { + pos.ExitConvergence = "价差收敛" + } + t.mu.Lock() t.blacklist[pos.Coin] = time.Now() t.mu.Unlock() - log.Printf("[Trader] ⛔ %s: Blacklisted — position open %.0fm without convergence", pos.Coin, time.Since(pos.StartedAt).Minutes()) + log.Printf("[Trader] ⛔ %s: Blacklisted — position open %.0fm without convergence | spread=%.4f%% netPnl=%.4f%%", pos.Coin, time.Since(pos.StartedAt).Minutes(), diffPct, netPnl) notifier.Send(fmt.Sprintf( "[黑名单] %s/USDT\n"+ " 开仓 %.0f 分钟未收敛\n"+ + " 价差: %.4f%% 净利: %.4f%%\n"+ " 已加入黑名单观察\n", - pos.Coin, time.Since(pos.StartedAt).Minutes())) + pos.Coin, time.Since(pos.StartedAt).Minutes(), diffPct, netPnl)) // Force-close the position immediately pos.Status = "close_failed" // triggers retryClose on next tick