117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
|
|
"""Metrics collector for the performance dashboard."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
from collections import deque
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class TradeMetric:
|
||
|
|
"""Single trade metric for time-series tracking."""
|
||
|
|
timestamp: float
|
||
|
|
asset: str
|
||
|
|
direction: str
|
||
|
|
timeframe: str
|
||
|
|
entry_price: float
|
||
|
|
size: int
|
||
|
|
edge: float
|
||
|
|
pnl: Optional[float] = None
|
||
|
|
won: Optional[bool] = None
|
||
|
|
|
||
|
|
|
||
|
|
class MetricsCollector:
|
||
|
|
"""Collects and aggregates trading metrics for the dashboard.
|
||
|
|
|
||
|
|
Maintains rolling windows and aggregated stats for real-time display.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, max_history: int = 10000) -> None:
|
||
|
|
self._trades: deque[TradeMetric] = deque(maxlen=max_history)
|
||
|
|
self._pnl_series: deque[tuple[float, float]] = deque(maxlen=max_history)
|
||
|
|
self._start_time = time.time()
|
||
|
|
|
||
|
|
# Running aggregates
|
||
|
|
self.total_volume: float = 0.0
|
||
|
|
self.total_fees: float = 0.0
|
||
|
|
|
||
|
|
def record_trade(self, metric: TradeMetric) -> None:
|
||
|
|
self._trades.append(metric)
|
||
|
|
self.total_volume += metric.entry_price * metric.size
|
||
|
|
|
||
|
|
def record_pnl(self, pnl: float) -> None:
|
||
|
|
self._pnl_series.append((time.time(), pnl))
|
||
|
|
|
||
|
|
def record_fee(self, fee: float) -> None:
|
||
|
|
self.total_fees += fee
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
# Aggregations
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
|
||
|
|
def get_recent_trades(self, n: int = 50) -> list[TradeMetric]:
|
||
|
|
return list(self._trades)[-n:]
|
||
|
|
|
||
|
|
def get_pnl_series(self) -> list[tuple[float, float]]:
|
||
|
|
return list(self._pnl_series)
|
||
|
|
|
||
|
|
def get_hourly_stats(self) -> dict:
|
||
|
|
"""Aggregate stats for the last hour."""
|
||
|
|
cutoff = time.time() - 3600
|
||
|
|
recent = [t for t in self._trades if t.timestamp > cutoff]
|
||
|
|
|
||
|
|
wins = sum(1 for t in recent if t.won is True)
|
||
|
|
losses = sum(1 for t in recent if t.won is False)
|
||
|
|
total_pnl = sum(t.pnl for t in recent if t.pnl is not None)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"trades": len(recent),
|
||
|
|
"wins": wins,
|
||
|
|
"losses": losses,
|
||
|
|
"win_rate": wins / (wins + losses) * 100 if (wins + losses) > 0 else 0,
|
||
|
|
"pnl": round(total_pnl, 2),
|
||
|
|
}
|
||
|
|
|
||
|
|
def get_asset_breakdown(self) -> dict[str, dict]:
|
||
|
|
"""PnL and trade count per asset."""
|
||
|
|
breakdown: dict[str, dict] = {}
|
||
|
|
for t in self._trades:
|
||
|
|
if t.asset not in breakdown:
|
||
|
|
breakdown[t.asset] = {"trades": 0, "wins": 0, "pnl": 0.0}
|
||
|
|
breakdown[t.asset]["trades"] += 1
|
||
|
|
if t.won is True:
|
||
|
|
breakdown[t.asset]["wins"] += 1
|
||
|
|
if t.pnl is not None:
|
||
|
|
breakdown[t.asset]["pnl"] += t.pnl
|
||
|
|
|
||
|
|
for asset in breakdown:
|
||
|
|
total = breakdown[asset]["trades"]
|
||
|
|
wins = breakdown[asset]["wins"]
|
||
|
|
breakdown[asset]["win_rate"] = round(wins / total * 100, 1) if total > 0 else 0
|
||
|
|
breakdown[asset]["pnl"] = round(breakdown[asset]["pnl"], 2)
|
||
|
|
|
||
|
|
return breakdown
|
||
|
|
|
||
|
|
def get_uptime(self) -> float:
|
||
|
|
"""Return uptime in seconds."""
|
||
|
|
return time.time() - self._start_time
|
||
|
|
|
||
|
|
def get_summary(self) -> dict:
|
||
|
|
total_trades = len(self._trades)
|
||
|
|
wins = sum(1 for t in self._trades if t.won is True)
|
||
|
|
losses = sum(1 for t in self._trades if t.won is False)
|
||
|
|
total_pnl = sum(t.pnl for t in self._trades if t.pnl is not None)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"total_trades": total_trades,
|
||
|
|
"wins": wins,
|
||
|
|
"losses": losses,
|
||
|
|
"win_rate": round(wins / (wins + losses) * 100, 1) if (wins + losses) > 0 else 0,
|
||
|
|
"total_pnl": round(total_pnl, 2),
|
||
|
|
"total_volume": round(self.total_volume, 2),
|
||
|
|
"total_fees": round(self.total_fees, 2),
|
||
|
|
"uptime_hours": round(self.get_uptime() / 3600, 2),
|
||
|
|
}
|