| """ |
| Module de monitoring: logs, mΓ©triques, alertes, dashboard. |
| """ |
| import json |
| import logging |
| import time |
| from dataclasses import dataclass, field, asdict |
| from datetime import datetime |
| from typing import Optional |
|
|
| logger = logging.getLogger("polybot.monitor") |
|
|
|
|
| @dataclass |
| class MetricSnapshot: |
| timestamp: float |
| balance_usd: float |
| total_exposure: float |
| total_pnl: float |
| daily_pnl: float |
| num_positions: int |
| num_trades: int |
| max_drawdown: float |
| arb_opportunities: int = 0 |
| value_bet_signals: int = 0 |
| lf_signals: int = 0 |
|
|
|
|
| @dataclass |
| class AlertEvent: |
| timestamp: float |
| level: str |
| title: str |
| message: str |
| strategy: str = "" |
| metadata: dict = field(default_factory=dict) |
|
|
|
|
| class BotMonitor: |
| """ |
| Moniteur du bot avec historique des métriques et système d'alertes. |
| """ |
|
|
| def __init__(self): |
| self.metrics_history: list[MetricSnapshot] = [] |
| self.alerts: list[AlertEvent] = [] |
| self.start_time: float = time.time() |
| self._daily_reset_time: float = time.time() |
|
|
| def record_metrics(self, snapshot: MetricSnapshot): |
| """Enregistre un snapshot de mΓ©triques.""" |
| self.metrics_history.append(snapshot) |
| logger.info( |
| f"π Balance: ${snapshot.balance_usd:.2f} | " |
| f"PnL: ${snapshot.total_pnl:+.2f} | " |
| f"Positions: {snapshot.num_positions} | " |
| f"Trades: {snapshot.num_trades} | " |
| f"Drawdown: {snapshot.max_drawdown*100:.2f}%" |
| ) |
|
|
| def alert(self, level: str, title: str, message: str, |
| strategy: str = "", metadata: dict = None): |
| """Enregistre et logue une alerte.""" |
| event = AlertEvent( |
| timestamp=time.time(), |
| level=level, |
| title=title, |
| message=message, |
| strategy=strategy, |
| metadata=metadata or {}, |
| ) |
| self.alerts.append(event) |
|
|
| log_func = { |
| "INFO": logger.info, |
| "WARN": logger.warning, |
| "ERROR": logger.error, |
| }.get(level, logger.info) |
|
|
| log_func(f"π¨ [{level}] {title}: {message}") |
|
|
| def check_risk_alerts(self, portfolio_summary: dict, config): |
| """VΓ©rifie les conditions d'alerte basΓ©es sur le portefeuille.""" |
|
|
| |
| daily_pnl = portfolio_summary.get("daily_pnl", 0) |
| if daily_pnl < -config.max_daily_loss_usd * 0.5: |
| self.alert( |
| "WARN", |
| "Daily Loss Warning", |
| f"Daily PnL: ${daily_pnl:.2f} β approaching daily limit of ${config.max_daily_loss_usd:.2f}. " |
| f"Consider reducing position sizes.", |
| metadata={"daily_pnl": daily_pnl} |
| ) |
|
|
| if daily_pnl < -config.max_daily_loss_usd: |
| self.alert( |
| "ERROR", |
| "Daily Loss Limit Reached", |
| f"Daily PnL: ${daily_pnl:.2f} β HALTING trading. " |
| f"All new positions blocked until daily reset.", |
| metadata={"daily_pnl": daily_pnl} |
| ) |
|
|
| |
| dd_str = portfolio_summary.get("max_drawdown", "0%") |
| dd = float(dd_str.replace("%", "")) / 100 |
| if dd > 0.10: |
| self.alert( |
| "WARN", |
| "High Drawdown", |
| f"Max drawdown: {dd*100:.2f}% β consider reducing exposure. " |
| f"Reduce position sizes by 50% if drawdown exceeds 15%.", |
| metadata={"max_drawdown": dd} |
| ) |
|
|
| def get_performance_report(self) -> dict: |
| """Génère un rapport de performance complet.""" |
| if not self.metrics_history: |
| return {"status": "no data"} |
|
|
| latest = self.metrics_history[-1] |
| first = self.metrics_history[0] |
|
|
| uptime = time.time() - self.start_time |
| hours = uptime / 3600 |
|
|
| return { |
| "uptime_hours": round(hours, 2), |
| "starting_balance": round(first.balance_usd, 2), |
| "current_balance": round(latest.balance_usd, 2), |
| "total_pnl": round(latest.total_pnl, 2), |
| "return_pct": round(latest.total_pnl / first.balance_usd * 100, 2) if first.balance_usd > 0 else 0, |
| "max_drawdown_pct": round(latest.max_drawdown * 100, 2), |
| "total_trades": latest.num_trades, |
| "active_positions": latest.num_positions, |
| "pnl_per_hour": round(latest.total_pnl / hours, 2) if hours > 0 else 0, |
| "total_alerts": len(self.alerts), |
| "error_alerts": sum(1 for a in self.alerts if a.level == "ERROR"), |
| "warn_alerts": sum(1 for a in self.alerts if a.level == "WARN"), |
| "snapshots_recorded": len(self.metrics_history), |
| } |
|
|
| def export_metrics(self, filepath: str = "bot_metrics.json"): |
| """Exporte les mΓ©triques en JSON.""" |
| data = { |
| "performance": self.get_performance_report(), |
| "alerts": [asdict(a) for a in self.alerts[-50:]], |
| "metrics_history": [asdict(m) for m in self.metrics_history[-100:]], |
| } |
| with open(filepath, "w") as f: |
| json.dump(data, f, indent=2) |
| logger.info(f"Metrics exported to {filepath}") |
|
|
| def print_dashboard(self): |
| """Affiche un dashboard compact dans les logs.""" |
| report = self.get_performance_report() |
| if report.get("status") == "no data": |
| logger.info("π No metrics data yet") |
| return |
|
|
| dashboard = f""" |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| β POLYMARKET BOT DASHBOARD β |
| β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£ |
| β Uptime: {report['uptime_hours']:.1f}h |
| β Balance: ${report['current_balance']:,.2f} (start: ${report['starting_balance']:,.2f}) |
| β Total PnL: ${report['total_pnl']:+,.2f} ({report['return_pct']:+.2f}%) |
| β PnL/Hour: ${report['pnl_per_hour']:+,.2f} |
| β Max Drawdown: {report['max_drawdown_pct']:.2f}% |
| β Trades: {report['total_trades']} |
| β Positions: {report['active_positions']} |
| β Alerts: {report['total_alerts']} (β οΈ{report['warn_alerts']} β{report['error_alerts']}) |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ""" |
| logger.info(dashboard) |
|
|