bot.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. /**
  2. * Main bot orchestrator.
  3. * Manages the lifecycle of the trading pipeline, polling, and WebSocket feeds.
  4. */
  5. import type { BotConfig, CityConfig, ModelForecasts } from '@poly-weather/core';
  6. import { createChildLogger, loadConfig, TARGET_CITIES } from '@poly-weather/core';
  7. import { startModelPolling } from '@poly-weather/weather-engine';
  8. import {
  9. CLOBTradingClient,
  10. OrderManager,
  11. WebSocketFeed,
  12. scanWeatherMarkets,
  13. groupMarketsByEvent,
  14. PositionSyncService,
  15. } from '@poly-weather/market-engine';
  16. import { TradingPipeline } from './pipeline.js';
  17. import { HealthCheckServer } from './health-check.js';
  18. import {
  19. disconnectDb,
  20. getPendingTrades,
  21. updateTradeStatus,
  22. getUnsettledTrades,
  23. settleTradePnl,
  24. aggregateDailyMetrics,
  25. } from './db.js';
  26. const logger = createChildLogger('bot');
  27. function sleep(ms: number): Promise<void> {
  28. return new Promise((resolve) => setTimeout(resolve, ms));
  29. }
  30. export class WeatherArbitrageBot {
  31. private config: BotConfig;
  32. private pipeline!: TradingPipeline;
  33. private healthCheck!: HealthCheckServer;
  34. private positionSync!: PositionSyncService;
  35. private wsFeed: WebSocketFeed | null = null;
  36. private stopModelPolling: (() => void) | null = null;
  37. private running = false;
  38. private intervals: ReturnType<typeof setInterval>[] = [];
  39. constructor(config?: BotConfig) {
  40. this.config = config || loadConfig();
  41. }
  42. /**
  43. * Start the bot.
  44. */
  45. async start(): Promise<void> {
  46. logger.info(
  47. {
  48. dryRun: this.config.dryRun,
  49. cities: this.config.cities.length,
  50. logLevel: this.config.logLevel,
  51. },
  52. '🤖 Weather Arbitrage Bot starting...',
  53. );
  54. // Initialize CLOB client with Polymarket wallet config
  55. const clobClient = new CLOBTradingClient(
  56. this.config.polymarket,
  57. this.config.dryRun,
  58. );
  59. // Initialize CLOB auth (derive or load API keys)
  60. if (!this.config.dryRun) {
  61. await clobClient.initialize();
  62. }
  63. // Initialize order manager
  64. const orderManager = new OrderManager(clobClient, this.config.dryRun);
  65. // Initialize position sync
  66. this.positionSync = new PositionSyncService(
  67. clobClient,
  68. { getPendingTrades, updateTradeStatus, getUnsettledTrades, settleTradePnl },
  69. this.config.dryRun,
  70. );
  71. // Initialize pipeline
  72. this.pipeline = new TradingPipeline(
  73. orderManager,
  74. this.config.cities,
  75. this.config.dryRun,
  76. );
  77. // Start health check server
  78. this.healthCheck = new HealthCheckServer(this.config.dryRun);
  79. this.healthCheck.setAutoExecuteCityCallback((city, enabled) => {
  80. this.pipeline.setAutoExecuteCity(city, enabled);
  81. this.healthCheck.setAutoExecuteCities(this.pipeline.autoExecuteCities);
  82. });
  83. this.healthCheck.start(this.config.healthPort);
  84. this.running = true;
  85. // Sync positions, settle resolved markets, aggregate metrics on startup
  86. await this.positionSync.syncOnStartup();
  87. await this.positionSync.settleTrades();
  88. await aggregateDailyMetrics();
  89. // Run initial cycle
  90. logger.info('Running initial pipeline cycle...');
  91. await this.runCycle();
  92. // Start model polling on cron schedule
  93. this.stopModelPolling = startModelPolling(
  94. this.config.cities,
  95. async (_city: CityConfig, _forecasts: ModelForecasts) => {
  96. // On new model data, immediately re-evaluate
  97. if (this.running) {
  98. logger.info('New model data detected, running evaluation...');
  99. await this.runCycle();
  100. }
  101. },
  102. );
  103. // Start periodic full scan (every 2 minutes)
  104. const scanInterval = setInterval(async () => {
  105. if (this.running) {
  106. await this.runCycle();
  107. }
  108. }, 120_000);
  109. this.intervals.push(scanInterval);
  110. // Start position sync + settlement + metrics aggregation (every 60 seconds)
  111. const syncInterval = setInterval(async () => {
  112. if (this.running) {
  113. try {
  114. if (!this.config.dryRun) {
  115. await this.positionSync.syncPendingOrders();
  116. }
  117. await this.positionSync.settleTrades();
  118. await aggregateDailyMetrics();
  119. } catch (err) {
  120. logger.error({ err }, 'Position sync / settlement / metrics failed');
  121. }
  122. }
  123. }, 60_000);
  124. this.intervals.push(syncInterval);
  125. // Start market re-discovery (every 30 minutes)
  126. const discoveryInterval = setInterval(async () => {
  127. if (this.running) {
  128. try {
  129. logger.info('Refreshing market discovery...');
  130. const newMarkets = await scanWeatherMarkets();
  131. const grouped = groupMarketsByEvent(newMarkets);
  132. logger.info({ marketCount: grouped.size }, 'Markets refreshed');
  133. // Update WebSocket subscriptions
  134. if (this.wsFeed) {
  135. const tokenIds = [...grouped.values()].flatMap((m) =>
  136. m.buckets.flatMap((b) => [b.yesTokenId, b.noTokenId]),
  137. );
  138. this.wsFeed.updateSubscriptions(tokenIds);
  139. }
  140. } catch (err) {
  141. logger.error({ err }, 'Market refresh failed');
  142. }
  143. }
  144. }, 1_800_000);
  145. this.intervals.push(discoveryInterval);
  146. logger.info(
  147. {
  148. dryRun: this.config.dryRun,
  149. healthPort: this.config.healthPort,
  150. },
  151. '✅ Bot is running',
  152. );
  153. }
  154. private async runCycle(): Promise<void> {
  155. try {
  156. const result = await this.pipeline.runCycle();
  157. this.healthCheck.updateCycleMetrics(result);
  158. logger.info(
  159. {
  160. markets: result.marketsEvaluated,
  161. signals: result.signalsGenerated,
  162. orders: result.ordersPlaced,
  163. errors: result.errors.length,
  164. },
  165. 'Cycle complete',
  166. );
  167. } catch (err) {
  168. logger.error({ err }, 'Cycle failed');
  169. }
  170. }
  171. /** Toggle auto-execute for a specific city at runtime. */
  172. setAutoExecuteCity(city: string, enabled: boolean): void {
  173. if (this.pipeline) {
  174. this.pipeline.setAutoExecuteCity(city, enabled);
  175. }
  176. }
  177. /** Get cities with auto-execute enabled. */
  178. get autoExecuteCities(): string[] {
  179. return this.pipeline?.autoExecuteCities ?? [];
  180. }
  181. /**
  182. * Stop the bot gracefully.
  183. */
  184. async stop(): Promise<void> {
  185. logger.info('Stopping bot...');
  186. this.running = false;
  187. // Stop intervals
  188. this.intervals.forEach(clearInterval);
  189. this.intervals = [];
  190. // Stop model polling
  191. if (this.stopModelPolling) {
  192. this.stopModelPolling();
  193. }
  194. // Disconnect WebSocket
  195. if (this.wsFeed) {
  196. this.wsFeed.disconnect();
  197. }
  198. // Stop health check
  199. this.healthCheck.stop();
  200. // Disconnect database
  201. await disconnectDb();
  202. logger.info('🛑 Bot stopped');
  203. }
  204. }