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
This commit is contained in:
jackyu66git
2026-05-03 21:13:11 +08:00
parent 1e4a3f3b37
commit 32f06b57d6
2 changed files with 61 additions and 57 deletions
+61 -1
View File
@@ -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,
-56
View File
@@ -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()