doulfa commited on
Commit
08547ef
·
verified ·
1 Parent(s): 77f0b05

Add bot orchestrator

Browse files
Files changed (1) hide show
  1. polymarket_bot/bot.py +285 -0
polymarket_bot/bot.py ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bot principal: orchestre les stratégies, l'exécution et le monitoring.
3
+ """
4
+ import asyncio
5
+ import logging
6
+ import signal
7
+ import sys
8
+ import time
9
+ from datetime import datetime
10
+ from typing import Optional
11
+
12
+ from .config import BotConfig
13
+ from .data import GammaClient, CLOBDataClient, Market
14
+ from .execution import ExecutionEngine
15
+ from .strategies import ArbitrageStrategy, ValueBetStrategy, LeaderFollowerStrategy, Signal
16
+ from .monitor import BotMonitor, MetricSnapshot
17
+
18
+ logger = logging.getLogger("polybot")
19
+
20
+
21
+ class PolymarketBot:
22
+ """
23
+ Bot de trading Polymarket ultra efficace.
24
+
25
+ Stratégies:
26
+ 1. Arbitrage intra-marché (sans risque, YES+NO < $1)
27
+ 2. Value Bet (signaux de marché + Kelly sizing)
28
+ 3. Leader-Follower sémantique (marchés corrélés)
29
+
30
+ Fonctionnalités:
31
+ - Gestion des risques multi-niveaux
32
+ - Mode dry-run / live
33
+ - Monitoring temps réel avec alertes
34
+ - Filtrage intelligent des marchés
35
+ """
36
+
37
+ def __init__(self, config: Optional[BotConfig] = None):
38
+ self.config = config or BotConfig()
39
+ self._setup_logging()
40
+
41
+ # Composants
42
+ self.gamma = GammaClient()
43
+ self.clob = CLOBDataClient()
44
+ self.engine = ExecutionEngine(self.config)
45
+ self.monitor = BotMonitor()
46
+
47
+ # Stratégies
48
+ self.strategies = {}
49
+ if "arbitrage" in self.config.strategies:
50
+ self.strategies["arbitrage"] = ArbitrageStrategy(self.config, self.clob)
51
+ if "value_bet" in self.config.strategies:
52
+ self.strategies["value_bet"] = ValueBetStrategy(self.config, self.clob)
53
+ if "leader_follower" in self.config.strategies:
54
+ self.strategies["leader_follower"] = LeaderFollowerStrategy(self.config, self.clob)
55
+
56
+ # State
57
+ self._markets: list[Market] = []
58
+ self._running = False
59
+ self._last_market_refresh = 0
60
+ self._cycle_count = 0
61
+
62
+ logger.info(f"Bot initialized | Mode: {'DRY RUN' if self.config.dry_run else 'LIVE'}")
63
+ logger.info(f"Strategies: {list(self.strategies.keys())}")
64
+ logger.info(f"Max exposure: ${self.config.max_total_exposure_usd:,.2f}")
65
+
66
+ def _setup_logging(self):
67
+ """Configure le logging."""
68
+ log_level = getattr(logging, self.config.log_level, logging.INFO)
69
+ logging.basicConfig(
70
+ level=log_level,
71
+ format="%(asctime)s | %(name)-20s | %(levelname)-5s | %(message)s",
72
+ datefmt="%Y-%m-%d %H:%M:%S",
73
+ )
74
+
75
+ # ── Market Management ────────────────────────────────────────
76
+ async def refresh_markets(self):
77
+ """Rafraîchit la liste des marchés depuis Gamma API."""
78
+ logger.info("🔄 Refreshing markets...")
79
+ all_markets = await self.gamma.get_all_active_markets()
80
+
81
+ # Filtrer selon la config
82
+ filtered = []
83
+ for m in all_markets:
84
+ if m.volume < self.config.min_market_volume:
85
+ continue
86
+ if any(tag in self.config.excluded_tags for tag in m.tags):
87
+ continue
88
+ if m.closed or not m.active:
89
+ continue
90
+ if not m.yes_token or not m.no_token:
91
+ continue
92
+ filtered.append(m)
93
+
94
+ self._markets = filtered
95
+ self._last_market_refresh = time.time()
96
+ logger.info(f"✅ {len(filtered)} markets loaded (from {len(all_markets)} total)")
97
+
98
+ if len(filtered) < 10:
99
+ self.monitor.alert(
100
+ "WARN",
101
+ "Low Market Count",
102
+ f"Only {len(filtered)} markets pass filters. Consider relaxing min_market_volume.",
103
+ )
104
+
105
+ def _should_refresh_markets(self) -> bool:
106
+ return (time.time() - self._last_market_refresh) > self.config.market_refresh_interval_seconds
107
+
108
+ # ── Core Trading Loop ────────────────────────────────────────
109
+ async def _trading_cycle(self):
110
+ """Un cycle complet de trading: scan → signal → risque → exécution."""
111
+ self._cycle_count += 1
112
+
113
+ if self._should_refresh_markets():
114
+ await self.refresh_markets()
115
+
116
+ if not self._markets:
117
+ logger.warning("No markets available, skipping cycle")
118
+ return
119
+
120
+ all_signals: list[Signal] = []
121
+
122
+ # Scanner toutes les stratégies en parallèle
123
+ scan_tasks = []
124
+ for name, strategy in self.strategies.items():
125
+ scan_tasks.append(strategy.scan(self._markets))
126
+
127
+ results = await asyncio.gather(*scan_tasks, return_exceptions=True)
128
+
129
+ for i, (name, _) in enumerate(self.strategies.items()):
130
+ if isinstance(results[i], Exception):
131
+ logger.error(f"Strategy {name} scan failed: {results[i]}")
132
+ self.monitor.alert("ERROR", f"{name} Scan Failed", str(results[i]), strategy=name)
133
+ elif results[i]:
134
+ all_signals.extend(results[i])
135
+
136
+ if not all_signals:
137
+ if self._cycle_count % 30 == 0:
138
+ logger.debug(f"Cycle {self._cycle_count}: no signals (scanning {len(self._markets)} markets)")
139
+ return
140
+
141
+ # Trier les signaux par profit attendu * confiance
142
+ all_signals.sort(key=lambda s: s.expected_profit * s.confidence, reverse=True)
143
+
144
+ # Exécuter les meilleurs signaux
145
+ executed = 0
146
+ for signal in all_signals[:5]:
147
+ strategy = self.strategies.get(signal.strategy)
148
+ if not strategy:
149
+ continue
150
+
151
+ try:
152
+ trade = await strategy.execute(signal, self.engine)
153
+ if trade:
154
+ executed += 1
155
+ self.monitor.alert(
156
+ "INFO",
157
+ f"Trade Executed ({signal.strategy})",
158
+ f"{signal.action} | Confidence: {signal.confidence:.2f} | "
159
+ f"Expected profit: ${signal.expected_profit:.2f} | "
160
+ f"Size: ${signal.size_usd:.2f}",
161
+ strategy=signal.strategy,
162
+ metadata=signal.metadata or {},
163
+ )
164
+ except Exception as e:
165
+ logger.error(f"Execution failed for signal: {e}")
166
+ self.monitor.alert("ERROR", "Execution Failed", str(e), strategy=signal.strategy)
167
+
168
+ if executed > 0:
169
+ logger.info(f"✅ Cycle {self._cycle_count}: {executed}/{len(all_signals)} signals executed")
170
+
171
+ async def _monitor_cycle(self):
172
+ """Cycle de monitoring: métriques, alertes, dashboard."""
173
+ summary = self.engine.get_portfolio_summary()
174
+
175
+ snapshot = MetricSnapshot(
176
+ timestamp=time.time(),
177
+ balance_usd=summary["balance_usd"],
178
+ total_exposure=summary["total_exposure"],
179
+ total_pnl=summary["total_pnl"],
180
+ daily_pnl=summary["daily_pnl"],
181
+ num_positions=summary["num_positions"],
182
+ num_trades=summary["num_trades"],
183
+ max_drawdown=float(summary["max_drawdown"].replace("%", "")) / 100,
184
+ )
185
+ self.monitor.record_metrics(snapshot)
186
+ self.monitor.check_risk_alerts(summary, self.config)
187
+
188
+ # ── Main Loop ────────────────────────────────────────────────
189
+ async def run(self, max_cycles: int = None, duration_seconds: int = None):
190
+ """
191
+ Lance le bot.
192
+
193
+ Args:
194
+ max_cycles: Nombre max de cycles (None = infini)
195
+ duration_seconds: Durée max en secondes (None = infini)
196
+ """
197
+ self._running = True
198
+ start_time = time.time()
199
+
200
+ banner = f"""
201
+ ╔══════════════════════════════════════════════════════════════╗
202
+ ║ 🤖 POLYMARKET ULTRA BOT — STARTED 🤖 ║
203
+ ╠══════════════════════════════════════════════════════════════╣
204
+ ║ Mode: {'DRY RUN (Paper Trading)' if self.config.dry_run else 'LIVE TRADING ⚠️'}
205
+ ║ Strategies: {', '.join(self.strategies.keys())}
206
+ ║ Max Exposure: ${self.config.max_total_exposure_usd:,.2f}
207
+ ║ Poll Interval: {self.config.poll_interval_seconds}s
208
+ ║ Capital: ${self.engine.portfolio.balance_usd:,.2f}
209
+ ║ Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
210
+ ╚══════════════════════════════════════════════════════════════╝"""
211
+ logger.info(banner)
212
+
213
+ await self.refresh_markets()
214
+
215
+ cycle = 0
216
+ monitor_interval = 30
217
+
218
+ try:
219
+ while self._running:
220
+ cycle += 1
221
+
222
+ if max_cycles and cycle > max_cycles:
223
+ logger.info(f"Max cycles reached ({max_cycles})")
224
+ break
225
+
226
+ if duration_seconds and (time.time() - start_time) > duration_seconds:
227
+ logger.info(f"Duration limit reached ({duration_seconds}s)")
228
+ break
229
+
230
+ try:
231
+ await self._trading_cycle()
232
+ except Exception as e:
233
+ logger.error(f"Trading cycle error: {e}")
234
+ self.monitor.alert("ERROR", "Cycle Error", str(e))
235
+
236
+ if cycle % monitor_interval == 0:
237
+ await self._monitor_cycle()
238
+ self.monitor.print_dashboard()
239
+
240
+ await asyncio.sleep(self.config.poll_interval_seconds)
241
+
242
+ except KeyboardInterrupt:
243
+ logger.info("Bot stopped by user (Ctrl+C)")
244
+ except Exception as e:
245
+ logger.error(f"Fatal error: {e}")
246
+ self.monitor.alert("ERROR", "Fatal Error", str(e))
247
+ finally:
248
+ await self.shutdown()
249
+
250
+ async def shutdown(self):
251
+ """Arrêt propre du bot."""
252
+ logger.info("Shutting down bot...")
253
+ self._running = False
254
+
255
+ await self._monitor_cycle()
256
+ self.monitor.print_dashboard()
257
+ self.monitor.export_metrics("bot_metrics.json")
258
+
259
+ report = self.monitor.get_performance_report()
260
+ logger.info(f"""
261
+ ╔══════════════════════════════════════════════════════════════╗
262
+ ║ 📈 FINAL PERFORMANCE REPORT 📈 ║
263
+ ╠══════════════════════════════════════════════════════════════╣
264
+ ║ Runtime: {report.get('uptime_hours', 0):.2f} hours
265
+ ║ Final Balance: ${report.get('current_balance', 0):,.2f}
266
+ ║ Total PnL: ${report.get('total_pnl', 0):+,.2f} ({report.get('return_pct', 0):+.2f}%)
267
+ ║ Max Drawdown: {report.get('max_drawdown_pct', 0):.2f}%
268
+ ║ Total Trades: {report.get('total_trades', 0)}
269
+ ║ PnL/Hour: ${report.get('pnl_per_hour', 0):+,.2f}
270
+ ║ Alerts: {report.get('total_alerts', 0)} (⚠️{report.get('warn_alerts', 0)} ❌{report.get('error_alerts', 0)})
271
+ ╚══════════════════════════════════════════════════════════════╝""")
272
+
273
+ await self.gamma.close()
274
+ await self.clob.close()
275
+
276
+ logger.info("Bot shutdown complete ✅")
277
+
278
+ # ── Quick Start ──────────────────────────────────────────────
279
+ @classmethod
280
+ async def quick_start(cls, dry_run: bool = True, duration: int = 3600):
281
+ """Démarrage rapide du bot."""
282
+ config = BotConfig(dry_run=dry_run)
283
+ bot = cls(config)
284
+ await bot.run(duration_seconds=duration)
285
+ return bot