From 32f06b57d62c4956770e86941dae257a556e6c27 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sun, 3 May 2026 21:13:11 +0800 Subject: [PATCH] move DetailedStats calc out of trader.go into dashboard.go - Remove DetailedStats struct and GetDetailedStats() method from trader.go - Add calcDetailedStats() standalone pure function in dashboard.go - Dashboard calls d.trader.GetClosedTrades() + calcDetailedStats() - Trading logic now has zero display-oriented calculations --- dashboard.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++- trader.go | 56 ----------------------------------------------- 2 files changed, 61 insertions(+), 57 deletions(-) diff --git a/dashboard.go b/dashboard.go index 3d6586d..dcf769b 100644 --- a/dashboard.go +++ b/dashboard.go @@ -246,6 +246,66 @@ func (d *Dashboard) Run() { } } +// ============================================================ +// Stats computation — kept separate from trading logic +// ============================================================ + +// DetailedStats holds aggregated PnL and duration statistics. +type DetailedStats struct { + TotalTrades int `json:"total_trades"` + TotalPnlPct float64 `json:"total_pnl_pct"` + AvgPnlPct float64 `json:"avg_pnl_pct"` + MaxProfitPct float64 `json:"max_profit_pct"` + MaxLossPct float64 `json:"max_loss_pct"` + AvgDuration string `json:"avg_duration"` + TotalDuration string `json:"total_duration"` + WinningTrades int `json:"winning_trades"` + LosingTrades int `json:"losing_trades"` + WinRate float64 `json:"win_rate"` +} + +// calcDetailedStats computes trading statistics from a slice of closed trades. +// This is a pure function — no dependency on Trader internals. +func calcDetailedStats(trades []TradeRecord) DetailedStats { + ds := DetailedStats{} + if len(trades) == 0 { + return ds + } + var totalDur time.Duration + ds.MaxLossPct = 1e9 // sentinel + for _, tr := range trades { + ds.TotalTrades++ + ds.TotalPnlPct += tr.PnlPct + if tr.PnlPct >= 0 { + ds.WinningTrades++ + if tr.PnlPct > ds.MaxProfitPct { + ds.MaxProfitPct = tr.PnlPct + } + } else { + ds.LosingTrades++ + if tr.PnlPct < ds.MaxLossPct { + ds.MaxLossPct = tr.PnlPct + } + } + if !tr.ClosedAt.IsZero() && !tr.OpenedAt.IsZero() { + totalDur += tr.ClosedAt.Sub(tr.OpenedAt) + } + } + if ds.MaxLossPct == 1e9 { + ds.MaxLossPct = 0 + } + if ds.TotalTrades > 0 { + ds.AvgPnlPct = ds.TotalPnlPct / float64(ds.TotalTrades) + ds.WinRate = float64(ds.WinningTrades) / float64(ds.TotalTrades) * 100 + } + if totalDur > 0 { + avgDur := totalDur / time.Duration(ds.TotalTrades) + ds.AvgDuration = avgDur.Round(time.Second).String() + ds.TotalDuration = totalDur.Round(time.Second).String() + } + return ds +} + // broadcastLoop pushes data to SSE clients every 1 second. func (d *Dashboard) broadcastLoop() { tick := time.NewTicker(1 * time.Second) @@ -360,7 +420,7 @@ func (d *Dashboard) broadcastLoop() { // 4. Stats + connection status (P3-5) converged, diverged, flat, total := d.trader.GetClosedStats() - detail := d.trader.GetDetailedStats() + detail := calcDetailedStats(d.trader.GetClosedTrades()) stats := map[string]interface{}{ "total_trades": total, "converged": converged, diff --git a/trader.go b/trader.go index 6c7a82a..f7cf14a 100644 --- a/trader.go +++ b/trader.go @@ -753,62 +753,6 @@ func (t *Trader) GetClosedStats() (converged, diverged, flat, total int) { return } -// GetDetailedStats returns comprehensive trading statistics. -type DetailedStats struct { - TotalTrades int `json:"total_trades"` - TotalPnlPct float64 `json:"total_pnl_pct"` - AvgPnlPct float64 `json:"avg_pnl_pct"` - MaxProfitPct float64 `json:"max_profit_pct"` - MaxLossPct float64 `json:"max_loss_pct"` - AvgDuration string `json:"avg_duration"` - TotalDuration string `json:"total_duration"` - WinningTrades int `json:"winning_trades"` - LosingTrades int `json:"losing_trades"` - WinRate float64 `json:"win_rate"` -} - -func (t *Trader) GetDetailedStats() DetailedStats { - t.mu.Lock() - defer t.mu.Unlock() - ds := DetailedStats{} - if len(t.closedTrades) == 0 { - return ds - } - var totalDur time.Duration - ds.MaxLossPct = 1e9 // sentinel - for _, tr := range t.closedTrades { - ds.TotalTrades++ - ds.TotalPnlPct += tr.PnlPct - if tr.PnlPct >= 0 { - ds.WinningTrades++ - if tr.PnlPct > ds.MaxProfitPct { - ds.MaxProfitPct = tr.PnlPct - } - } else { - ds.LosingTrades++ - if tr.PnlPct < ds.MaxLossPct { - ds.MaxLossPct = tr.PnlPct - } - } - if !tr.ClosedAt.IsZero() && !tr.OpenedAt.IsZero() { - totalDur += tr.ClosedAt.Sub(tr.OpenedAt) - } - } - if ds.MaxLossPct == 1e9 { - ds.MaxLossPct = 0 - } - if ds.TotalTrades > 0 { - ds.AvgPnlPct = ds.TotalPnlPct / float64(ds.TotalTrades) - ds.WinRate = float64(ds.WinningTrades) / float64(ds.TotalTrades) * 100 - } - if totalDur > 0 { - avgDur := totalDur / time.Duration(ds.TotalTrades) - ds.AvgDuration = avgDur.Round(time.Second).String() - ds.TotalDuration = totalDur.Round(time.Second).String() - } - return ds -} - // GetClosedTrades returns the full closed trade history. func (t *Trader) GetClosedTrades() []TradeRecord { t.mu.Lock()