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:
+61
-1
@@ -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.
|
// broadcastLoop pushes data to SSE clients every 1 second.
|
||||||
func (d *Dashboard) broadcastLoop() {
|
func (d *Dashboard) broadcastLoop() {
|
||||||
tick := time.NewTicker(1 * time.Second)
|
tick := time.NewTicker(1 * time.Second)
|
||||||
@@ -360,7 +420,7 @@ func (d *Dashboard) broadcastLoop() {
|
|||||||
|
|
||||||
// 4. Stats + connection status (P3-5)
|
// 4. Stats + connection status (P3-5)
|
||||||
converged, diverged, flat, total := d.trader.GetClosedStats()
|
converged, diverged, flat, total := d.trader.GetClosedStats()
|
||||||
detail := d.trader.GetDetailedStats()
|
detail := calcDetailedStats(d.trader.GetClosedTrades())
|
||||||
stats := map[string]interface{}{
|
stats := map[string]interface{}{
|
||||||
"total_trades": total,
|
"total_trades": total,
|
||||||
"converged": converged,
|
"converged": converged,
|
||||||
|
|||||||
@@ -753,62 +753,6 @@ func (t *Trader) GetClosedStats() (converged, diverged, flat, total int) {
|
|||||||
return
|
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.
|
// GetClosedTrades returns the full closed trade history.
|
||||||
func (t *Trader) GetClosedTrades() []TradeRecord {
|
func (t *Trader) GetClosedTrades() []TradeRecord {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user