doulfa's picture
Add monitor module
de0dde6 verified
"""
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)