fix: 重启后总PnL回到0的问题 + 前端PnL图表公式统一
- trader.go: NewTrader 启动时从DB GetAllClosedTrades() 加载所有已平仓交易到 closedTrades - trader.go: 新增 safeFloat/safeStr 辅助函数处理DB空指针 - db/trade_repo.go: 新增 GetAllClosedTrades() 方法 - frontend: PnL图表标题和曲线改用 2*AmountUSD*NetPnl/100 公式,与后端Stats一致
This commit is contained in:
@@ -260,6 +260,20 @@ func (d *DB) GetScalePrices(tradeID int64) (longPrices, shortPrices []float64, e
|
|||||||
return longPrices, shortPrices, rows.Err()
|
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.
|
// GetClosedStats returns convergence counts from the database.
|
||||||
func (d *DB) GetClosedStats() (converged, diverged, flat, total int, err error) {
|
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 {
|
if err = d.QueryRow("SELECT COUNT(*) FROM trades WHERE status='closed'").Scan(&total); err != nil {
|
||||||
|
|||||||
@@ -562,7 +562,7 @@ function PnlChart() {
|
|||||||
.filter(t => t.ClosedAt && t.NetPnl != null)
|
.filter(t => t.ClosedAt && t.NetPnl != null)
|
||||||
.sort((a, b) => new Date(a.ClosedAt) - new Date(b.ClosedAt))
|
.sort((a, b) => new Date(a.ClosedAt) - new Date(b.ClosedAt))
|
||||||
setData(trades)
|
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)
|
setTotalPnl(total)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
@@ -596,7 +596,7 @@ function PnlChart() {
|
|||||||
const points = []
|
const points = []
|
||||||
let cum = 0
|
let cum = 0
|
||||||
for (const t of data) {
|
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 })
|
points.push({ x: new Date(t.ClosedAt).getTime(), y: cum })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,35 @@ func NewTrader(cfg *Config, database *db.DB) *Trader {
|
|||||||
// Restore open positions from DB on restart
|
// Restore open positions from DB on restart
|
||||||
if database != nil {
|
if database != nil {
|
||||||
t.restoreOpenPositions()
|
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
|
// Load historical closed trade stats for convergence display
|
||||||
if c, d, f, tot, err := database.GetClosedStats(); err == nil {
|
if c, d, f, tot, err := database.GetClosedStats(); err == nil {
|
||||||
t.dbConverged, t.dbDiverged, t.dbFlat, t.dbTotal = c, d, f, tot
|
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()
|
defer t.mu.Unlock()
|
||||||
delete(t.blacklist, coin)
|
delete(t.blacklist, coin)
|
||||||
log.Printf("[Trader] ✅ %s: Removed from 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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user