""" 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 # "INFO", "WARN", "ERROR" 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.""" # Alerte perte journaliΓ¨re 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} ) # Alerte drawdown 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)