""" FastAPI Backend — serves metrics and trade data to the React dashboard. """ import asyncio from contextlib import asynccontextmanager from datetime import datetime, timezone import os import re from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from bot.data.db import Database # Matches the feat_str embedded in reasoning for trades from bayesian.py v2+: # "fg=+0.0600 mom=+0.0000 news=+0.0000 mfld=-0.7483" _FEAT_RE = re.compile( r"fg=([+-]?[\d.]+).*?mom=([+-]?[\d.]+).*?news=([+-]?[\d.]+).*?mfld=([+-]?[\d.]+)" ) def _enrich_trade(trade: dict) -> dict: """Add days_open and signal_components to an open trade dict.""" ts = trade.get("timestamp") if ts is not None: now = datetime.now(timezone.utc) if getattr(ts, "tzinfo", None) is None: ts = ts.replace(tzinfo=timezone.utc) trade["days_open"] = round((now - ts).total_seconds() / 86400, 1) else: trade["days_open"] = None reasoning = trade.get("reasoning") or "" m = _FEAT_RE.search(reasoning) trade["signal_components"] = ( {"fg": float(m.group(1)), "mom": float(m.group(2)), "news": float(m.group(3)), "mfld": float(m.group(4))} if m else None ) return trade db = Database() @asynccontextmanager async def lifespan(app: FastAPI): await db.connect() yield await db.disconnect() app = FastAPI(title="Polymarket Bot API", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"], ) @app.get("/health") async def health(): return {"status": "ok", "paper_mode": os.getenv("PAPER_MODE", "true")} @app.get("/api/metrics") async def get_metrics(): history = await db.get_metrics_history(days=42) if not history: return {"history": [], "latest": None} return {"history": history, "latest": history[0]} @app.get("/api/trades") async def get_trades(limit: int = 50, status: str = "open"): """ status: "open" (default) | "closed" | "all" Open trades include days_open and signal_components {fg, mom, news, mfld}. """ if status not in ("open", "closed", "all"): status = "open" filter_status = None if status == "all" else status trades = await db.get_recent_trades(limit=limit, status=filter_status) if filter_status == "open": trades = [_enrich_trade(t) for t in trades] return {"trades": trades, "count": len(trades), "status_filter": status} @app.get("/api/summary") async def get_summary(): """Dashboard summary card data.""" history = await db.get_metrics_history(days=1) open_trades, all_trades, inverted, legacy_count = await asyncio.gather( db.get_recent_trades(limit=500, status="open"), db.get_recent_trades(limit=500), db.get_recently_closed_inverted(hours=24), db.get_legacy_incomplete_count(), ) latest = history[0] if history else {} paper_bankroll = float(os.getenv("PAPER_BANKROLL", "10000")) total_deployed = sum(t.get("net_cost", 0) for t in open_trades) return { "paper_mode": os.getenv("PAPER_MODE", "true") == "true", "paper_bankroll": paper_bankroll, "total_trades": len(all_trades), "open_trades_count": len(open_trades), "closed_trades_count": len(all_trades) - len(open_trades), "total_deployed": total_deployed, "cash_available": max(0.0, paper_bankroll - total_deployed), "legacy_incomplete_count": legacy_count, "reentry_guard_blocks_24h": len(inverted), # Metrics from latest metrics_daily snapshot (computed by MetricsTracker). # total_pnl: estimated unrealized PnL for open trades in the current bot # session — uses edge_net × net_cost (model edge on deployed # capital). Resets to 0 on pod restart until Fix 3 is applied. # calibration_score: null until market resolution data is available # (requires close_price / outcome per closed trade). "total_pnl": latest.get("total_pnl", 0), "win_rate": latest.get("win_rate", 0), "sharpe_ratio": latest.get("sharpe_ratio", 0), "calibration_score": latest.get("calibration_score"), # null if unavailable "promotion_ready": ( latest.get("sharpe_ratio", 0) >= 0.5 and latest.get("win_rate", 0) >= 0.52 and (latest.get("calibration_score") or 0) >= 0.7 # null → not ready and len(all_trades) >= 50 ), }