{"openapi":"3.1.0","info":{"title":"Whales Bet Signal Stream","description":"\nPublic real-time signal stream + historical analytics for the Whales Bet\nweather-market trading bot.\n\n## Architecture\n\n```\nTrading bot ─→ events.jsonl ─→ this server ─→ WSS clients\n                              └→ session_state.json ─→ HTTP /api/*\n```\n\n## How to integrate\n\n1. **Backfill on startup**: `GET /api/signals/recent?limit=500&resolved_only=true`\n   to ingest the bot's track record.\n2. **Pull stats**: `GET /api/signals/stats` for win-rate by city/edge/horizon —\n   lets you decide *which* signals to follow.\n3. **Subscribe live**: connect to `wss://signals.whales.bet/ws` and listen\n   for `signal_published` events. Each one tells you exactly which\n   Polymarket token to bet on, what side, what price, and the bot's edge.\n4. **Sync forecasts**: `forecast_update` events fire after each cycle (~1h)\n   with a multi-day ensemble per city.\n\n## WebSocket protocol\n\nConnect → optionally send subscribe message → receive event stream.\n\n```js\nconst ws = new WebSocket(\"wss://signals.whales.bet/ws\");\nws.onopen = () => ws.send(JSON.stringify({\n  type: \"subscribe\",\n  topics: [\"signal_published\", \"position_resolved\"]  // or \"*\"\n}));\nws.onmessage = e => {\n  const ev = JSON.parse(e.data);\n  // ev.type: \"hello\" | \"signal_published\" | \"trade_executed\"\n  //        | \"trade_failed\" | \"position_resolved\" | \"forecast_update\"\n  //        | \"cycle_complete\" | \"subscribed\" | \"pong\"\n};\n```\n\nFull AsyncAPI 3.0 schema available at **`/api/asyncapi.json`**.\n\n## Auth\n\nPublic by default. If `SIGNALS_API_KEY` is set on the server, pass\n`?api_key=<key>` (query) or `X-API-Key: <key>` (header) on every request,\nand `?api_key=<key>` on the WebSocket URL.\n","version":"1.1.0","x-asyncapi-spec":"https://signals.whales.bet/api/asyncapi.json","x-websocket-url":"wss://signals.whales.bet/ws"},"paths":{"/api/health":{"get":{"tags":["Health"],"summary":"Liveness probe","description":"Returns 200 with basic status info. No auth required.","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/stats":{"get":{"tags":["Stats"],"summary":"Current P&L snapshot","description":"Cumulative session stats: realized/unrealized P&L, wins, losses, win rate.","operationId":"stats_api_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}}}}},"/api/positions":{"get":{"tags":["Positions"],"summary":"Open positions","description":"Positions the bot is currently holding (not yet resolved or failed).","operationId":"positions_api_positions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenPositionsResponse"}}}}}}},"/api/history":{"get":{"tags":["Stream"],"summary":"Past event log","description":"Reads from events.jsonl. Same events that flow through the WebSocket — useful to backfill on client startup.","operationId":"history_api_history_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Max events to return","default":100,"title":"Limit"},"description":"Max events to return"},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by event type: signal_published, trade_executed, position_resolved, forecast_update, cycle_complete","title":"Type"},"description":"Filter by event type: signal_published, trade_executed, position_resolved, forecast_update, cycle_complete"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/calibration":{"get":{"tags":["Analytics"],"summary":"Per-dimension edge multipliers","description":"Postmortem-derived multipliers applied to edge per city/type/edge-band/horizon. Empty if not yet run.","operationId":"calibration_api_calibration_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Calibration Api Calibration Get"}}}}}}},"/api/forecasts":{"get":{"tags":["Forecasts"],"summary":"Latest weather ensembles","description":"Multi-day weather ensemble per tracked city. Refreshed every cycle (~60min).","operationId":"forecasts_api_forecasts_get","parameters":[{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"If set, returns only this city's multi-day forecast","title":"City"},"description":"If set, returns only this city's multi-day forecast"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForecastsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/signals/stats":{"get":{"tags":["Analytics"],"summary":"Win-rate breakdown","description":"Win rate + ROI of past resolved signals broken down by city, market type, edge band, horizon, and side.","operationId":"signals_stats_api_signals_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignalStatsResponse"}}}}}}},"/api/signals/recent":{"get":{"tags":["Positions"],"summary":"Recent trades","description":"Past trades newest-first with full detail: entry price, shares, edge, horizon, P&L if resolved.","operationId":"signals_recent_api_signals_recent_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"resolved_only","in":"query","required":false,"schema":{"type":"boolean","description":"Only return trades that already resolved","default":false,"title":"Resolved Only"},"description":"Only return trades that already resolved"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecentSignalsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/signals/probation":{"get":{"tags":["Analytics"],"summary":"City promotion/demotion view","description":"Quick view of cities under evaluation. Verdicts are ROI-DRIVEN (money),\nNOT win-rate driven: this book buys a lot of cheap YES, where a 35-45% WR\nis comfortably profitable (break-even WR at price P is just P), and\nexpensive NO, where a 70% WR can still be break-even. Judging by WR\ndemoted profitable cities (buenos-aires: 44% WR, +24% ROI on YES) and\n\"watched\" money-losers (tokyo: 62% WR, -22% ROI).\n  ✓ proven/promote — n>=10, ROI > +3%\n  ⏸ demote         — n>=10, ROI < -5%   (calibration auto-pauses <= -20%)\n  ⚠ watching       — n in [5,10), keep collecting\n  … too-few        — n<5\nUsed to decide which cities to keep in CITY_WHITELIST.","operationId":"probation_view_api_signals_probation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Probation View Api Signals Probation Get"}}}}}}},"/api/cities":{"get":{"tags":["Analytics"],"summary":"Aggregated by city","operationId":"cities_api_cities_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CitiesResponse"}}}}}}},"/api/asyncapi.json":{"get":{"tags":["Stream"],"summary":"AsyncAPI 3.0 spec for /ws","description":"Machine-readable WebSocket schema. Drop into AsyncAPI Studio or feed to an LLM.","operationId":"asyncapi_spec_api_asyncapi_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Asyncapi Spec Api Asyncapi Json Get"}}}}}}}},"components":{"schemas":{"CitiesResponse":{"properties":{"by_city":{"additionalProperties":{"$ref":"#/components/schemas/CityAgg"},"type":"object","title":"By City"}},"type":"object","required":["by_city"],"title":"CitiesResponse"},"CityAgg":{"properties":{"trades":{"type":"integer","title":"Trades"},"wins":{"type":"integer","title":"Wins"},"pnl":{"type":"number","title":"Pnl"},"win_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Win Rate"}},"type":"object","required":["trades","wins","pnl","win_rate"],"title":"CityAgg"},"DimensionStats":{"properties":{"n":{"type":"integer","title":"N"},"wins":{"type":"integer","title":"Wins"},"losses":{"type":"integer","title":"Losses"},"win_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Win Rate"},"pnl_usd":{"type":"number","title":"Pnl Usd"},"cost_usd":{"type":"number","title":"Cost Usd"},"roi":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Roi"}},"type":"object","required":["n","wins","losses","win_rate","pnl_usd","cost_usd","roi"],"title":"DimensionStats"},"ForecastDay":{"properties":{"date":{"type":"string","title":"Date"},"temp_max_mean_c":{"type":"number","title":"Temp Max Mean C"},"temp_max_std_c":{"type":"number","title":"Temp Max Std C"},"temp_min_mean_c":{"type":"number","title":"Temp Min Mean C"},"temp_min_std_c":{"type":"number","title":"Temp Min Std C"},"precip_prob_mean":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Precip Prob Mean"},"precip_prob_std":{"type":"number","title":"Precip Prob Std"},"sources_used":{"items":{"type":"string"},"type":"array","title":"Sources Used"},"n_sources":{"type":"integer","title":"N Sources"}},"type":"object","required":["date","temp_max_mean_c","temp_max_std_c","temp_min_mean_c","temp_min_std_c","precip_prob_mean","precip_prob_std","sources_used","n_sources"],"title":"ForecastDay"},"ForecastsResponse":{"properties":{"by_city":{"anyOf":[{"additionalProperties":{"items":{"$ref":"#/components/schemas/ForecastDay"},"type":"array"},"type":"object"},{"type":"null"}],"title":"By City"},"cities":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Cities"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"days":{"anyOf":[{"items":{"$ref":"#/components/schemas/ForecastDay"},"type":"array"},{"type":"null"}],"title":"Days"}},"type":"object","title":"ForecastsResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status","examples":["ok"]},"ts":{"type":"string","title":"Ts","description":"ISO8601 server timestamp"},"active_ws":{"type":"integer","title":"Active Ws","description":"WebSocket clients currently connected"},"events_file_exists":{"type":"boolean","title":"Events File Exists"}},"type":"object","required":["status","ts","active_ws","events_file_exists"],"title":"HealthResponse"},"HistoryEvent":{"properties":{"ts":{"type":"string","title":"Ts"},"seq":{"type":"integer","title":"Seq"},"type":{"type":"string","title":"Type","description":"signal_published, trade_executed, position_resolved, forecast_update, cycle_complete"},"payload":{"additionalProperties":true,"type":"object","title":"Payload"}},"type":"object","required":["ts","seq","type","payload"],"title":"HistoryEvent"},"HistoryResponse":{"properties":{"events":{"items":{"$ref":"#/components/schemas/HistoryEvent"},"type":"array","title":"Events"}},"type":"object","required":["events"],"title":"HistoryResponse"},"OpenPosition":{"properties":{"key":{"type":"string","title":"Key"},"side":{"type":"string","title":"Side","description":"BUY or SELL"},"target_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Token","description":"YES or NO"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"market_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Type","description":"temperature/precipitation/binary_weather"},"bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bucket"},"entry_price":{"type":"number","title":"Entry Price"},"shares":{"type":"number","title":"Shares"},"cost_usd":{"type":"number","title":"Cost Usd"},"edge_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Edge Pct"},"horizon_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Horizon Days"},"placed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Placed At"},"condition_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condition Id"},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id"}},"type":"object","required":["key","side","city","bucket","entry_price","shares","cost_usd","edge_pct","horizon_days","placed_at","condition_id","order_id"],"title":"OpenPosition"},"OpenPositionsResponse":{"properties":{"open":{"items":{"$ref":"#/components/schemas/OpenPosition"},"type":"array","title":"Open"}},"type":"object","required":["open"],"title":"OpenPositionsResponse"},"RecentSignal":{"properties":{"key":{"type":"string","title":"Key"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"market_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Market Type"},"bucket":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bucket"},"side":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Side"},"target_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target Token"},"entry_price":{"type":"number","title":"Entry Price"},"shares":{"type":"number","title":"Shares"},"cost_usd":{"type":"number","title":"Cost Usd"},"edge_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Edge Pct"},"horizon_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Horizon Days"},"resolved":{"type":"boolean","title":"Resolved"},"won":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Won"},"pnl_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pnl Usd"},"payout_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Payout Usd"},"placed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Placed At"},"resolved_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resolved At"},"order_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order Id"},"condition_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Condition Id"}},"type":"object","required":["key","city","market_type","bucket","side","target_token","entry_price","shares","cost_usd","edge_pct","horizon_days","resolved","won","pnl_usd","payout_usd","placed_at","resolved_at","order_id","condition_id"],"title":"RecentSignal"},"RecentSignalsResponse":{"properties":{"signals":{"items":{"$ref":"#/components/schemas/RecentSignal"},"type":"array","title":"Signals"}},"type":"object","required":["signals"],"title":"RecentSignalsResponse"},"SignalStatsResponse":{"properties":{"total_resolved":{"type":"integer","title":"Total Resolved"},"wins":{"type":"integer","title":"Wins"},"losses":{"type":"integer","title":"Losses"},"win_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Win Rate"},"by_city":{"additionalProperties":{"$ref":"#/components/schemas/DimensionStats"},"type":"object","title":"By City"},"by_market_type":{"additionalProperties":{"$ref":"#/components/schemas/DimensionStats"},"type":"object","title":"By Market Type"},"by_edge_band":{"additionalProperties":{"$ref":"#/components/schemas/DimensionStats"},"type":"object","title":"By Edge Band"},"by_horizon":{"additionalProperties":{"$ref":"#/components/schemas/DimensionStats"},"type":"object","title":"By Horizon"},"by_side":{"additionalProperties":{"$ref":"#/components/schemas/DimensionStats"},"type":"object","title":"By Side"},"by_city_market_type":{"additionalProperties":{"$ref":"#/components/schemas/DimensionStats"},"type":"object","title":"By City Market Type"},"ts":{"type":"string","title":"Ts"}},"type":"object","required":["total_resolved","wins","losses","win_rate","by_city","by_market_type","by_edge_band","by_horizon","by_side","by_city_market_type","ts"],"title":"SignalStatsResponse"},"StatsResponse":{"properties":{"realized_pnl_usd":{"type":"number","title":"Realized Pnl Usd","description":"Cumulative realized P&L in USD"},"unrealized_pnl_usd":{"type":"number","title":"Unrealized Pnl Usd"},"session_pnl_usd":{"type":"number","title":"Session Pnl Usd"},"total_wagered_usd":{"type":"number","title":"Total Wagered Usd","description":"Cumulative bet volume — same $ rolls many times"},"total_trades":{"type":"integer","title":"Total Trades"},"wins":{"type":"integer","title":"Wins"},"losses":{"type":"integer","title":"Losses"},"win_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Win Rate","description":"wins / (wins + losses)"},"open_positions":{"type":"integer","title":"Open Positions"},"starting_balance_usd":{"type":"number","title":"Starting Balance Usd","description":"Wallet balance when bot started"},"current_balance_usd":{"type":"number","title":"Current Balance Usd","description":"Starting + realized + unrealized"},"capital_growth_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capital Growth Pct","description":"Real growth on starting capital (e.g. +133% means doubled)"},"roi_per_trade_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Roi Per Trade Pct","description":"Per-trade margin (realized / wagered)"},"started_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Started At"},"last_cycle_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Cycle At"},"ts":{"type":"string","title":"Ts"}},"type":"object","required":["realized_pnl_usd","unrealized_pnl_usd","session_pnl_usd","total_wagered_usd","total_trades","wins","losses","open_positions","starting_balance_usd","current_balance_usd","started_at","last_cycle_at","ts"],"title":"StatsResponse"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}},"tags":[{"name":"Health","description":"Service status"},{"name":"Stats","description":"P&L, win rate, totals"},{"name":"Positions","description":"Open positions and history"},{"name":"Forecasts","description":"Live weather ensembles"},{"name":"Analytics","description":"Win-rate breakdowns across dimensions"},{"name":"Stream","description":"Live WebSocket event stream"}],"servers":[{"url":"https://signals.whales.bet","description":"Production"}],"externalDocs":{"description":"WebSocket schema (AsyncAPI 3.0)","url":"https://signals.whales.bet/api/asyncapi.json"}}