From ddaa5badd24492eea613e0be6709064df8aeb760 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Mon, 4 May 2026 06:23:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=87=8D=E5=90=AF=E5=90=8E=E6=80=BBPnL?= =?UTF-8?q?=E5=9B=9E=E5=88=B00=E7=9A=84=E9=97=AE=E9=A2=98=20+=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AFPnL=E5=9B=BE=E8=A1=A8=E5=85=AC=E5=BC=8F=E7=BB=9F?= =?UTF-8?q?=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - trader.go: NewTrader 启动时从DB GetAllClosedTrades() 加载所有已平仓交易到 closedTrades - trader.go: 新增 safeFloat/safeStr 辅助函数处理DB空指针 - db/trade_repo.go: 新增 GetAllClosedTrades() 方法 - frontend: PnL图表标题和曲线改用 2*AmountUSD*NetPnl/100 公式,与后端Stats一致 --- db/trade_repo.go | 14 ++++++++++++++ frontend/src/App.jsx | 4 ++-- trader.go | 45 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/db/trade_repo.go b/db/trade_repo.go index 7448a7d..52b7523 100644 --- a/db/trade_repo.go +++ b/db/trade_repo.go @@ -260,6 +260,20 @@ func (d *DB) GetScalePrices(tradeID int64) (longPrices, shortPrices []float64, e return longPrices, shortPrices, rows.Err() } +// GetAllClosedTrades returns all closed trades for PnL history restoration. +func (d *DB) GetAllClosedTrades() ([]TradeRecord, error) { + rows, err := d.Query(`SELECT id, coin, direction, status, entry_spread, exit_spread, + long_exchange, short_exchange, long_entry, long_exit, short_entry, short_exit, + long_pnl, short_pnl, fee_entry, fee_exit, net_pnl, + amount_usd, scale_count, exit_reason, convergence, opened_at, closed_at + FROM trades WHERE status='closed' ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + return scanTrades(rows) +} + // GetClosedStats returns convergence counts from the database. func (d *DB) GetClosedStats() (converged, diverged, flat, total int, err error) { if err = d.QueryRow("SELECT COUNT(*) FROM trades WHERE status='closed'").Scan(&total); err != nil { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index bcf421a..29a84f7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -562,7 +562,7 @@ function PnlChart() { .filter(t => t.ClosedAt && t.NetPnl != null) .sort((a, b) => new Date(a.ClosedAt) - new Date(b.ClosedAt)) setData(trades) - const total = trades.reduce((sum, t) => sum + (t.NetPnl || 0), 0) + const total = trades.reduce((sum, t) => sum + 2 * (t.AmountUSD || 0) * (t.NetPnl || 0) / 100, 0) setTotalPnl(total) } catch (e) {} } @@ -596,7 +596,7 @@ function PnlChart() { const points = [] let cum = 0 for (const t of data) { - cum += (t.AmountUSD || 0) * (t.NetPnl || 0) / 100 + cum += 2 * (t.AmountUSD || 0) * (t.NetPnl || 0) / 100 points.push({ x: new Date(t.ClosedAt).getTime(), y: cum }) } diff --git a/trader.go b/trader.go index a3f1dad..ab57cfd 100644 --- a/trader.go +++ b/trader.go @@ -183,6 +183,35 @@ func NewTrader(cfg *Config, database *db.DB) *Trader { // Restore open positions from DB on restart if database != nil { t.restoreOpenPositions() + + // Load historical closed trades for PnL stats (so total PnL survives restart) + if closed, err := database.GetAllClosedTrades(); err == nil { + for i := range closed { + dbTr := &closed[i] + pnlPct := safeFloat(dbTr.NetPnl) + pnlUSD := 2 * dbTr.AmountUSD * pnlPct / 100 + closedAt := time.Time{} + if dbTr.ClosedAt != nil { + closedAt = *dbTr.ClosedAt + } + record := TradeRecord{ + Coin: dbTr.Coin, + Direction: dbTr.Direction, + EntrySpread: safeFloat(dbTr.EntrySpread), + ExitSpread: safeFloat(dbTr.ExitSpread), + PnlPct: pnlPct, + PnlUSD: pnlUSD, + Convergence: safeStr(dbTr.Convergence), + Reason: safeStr(dbTr.ExitReason), + Duration: closedAt.Sub(dbTr.OpenedAt).Round(time.Second).String(), + OpenedAt: dbTr.OpenedAt, + ClosedAt: closedAt, + ScaleLevels: dbTr.ScaleCount, + AmountUSD: dbTr.AmountUSD, + } + t.closedTrades = append(t.closedTrades, record) + } + } // Load historical closed trade stats for convergence display if c, d, f, tot, err := database.GetClosedStats(); err == nil { t.dbConverged, t.dbDiverged, t.dbFlat, t.dbTotal = c, d, f, tot @@ -1282,4 +1311,20 @@ func (t *Trader) RemoveBlacklist(coin string) { defer t.mu.Unlock() delete(t.blacklist, coin) log.Printf("[Trader] ✅ %s: Removed from blacklist", coin) +} + +// safeFloat returns 0 for nil float64 pointers (DB nullable fields). +func safeFloat(f *float64) float64 { + if f == nil { + return 0 + } + return *f +} + +// safeStr returns empty string for nil string pointers (DB nullable fields). +func safeStr(s *string) string { + if s == nil { + return "" + } + return *s } \ No newline at end of file