from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from fastapi.templating import Jinja2Templates from sqlalchemy import func from datetime import datetime, timedelta import threading from db import init_db, SessionLocal, BotDecision, TradeExecution, CuratedInsight from bot import start_scheduler, run_cycle, curate_cycle from services import account_snapshot app = FastAPI(title="alpaca-llm-bot-v1") templates = Jinja2Templates(directory="templates") @app.on_event("startup") def startup(): init_db() start_scheduler() @app.get("/health") def health(): return {"ok": True} @app.post("/run-now") def run_now(): threading.Thread(target=run_cycle, daemon=True).start() return {"ok": True, "queued": True, "task": "run_cycle"} @app.post("/curate-now") def curate_now(): threading.Thread(target=curate_cycle, daemon=True).start() return {"ok": True, "queued": True, "task": "curate_cycle"} @app.get("/api/account") def api_account(): return JSONResponse(account_snapshot()) @app.get("/api/metrics") def api_metrics(hours: int = 72): db = SessionLocal() try: since = datetime.utcnow() - timedelta(hours=hours) decs = db.query(BotDecision).filter(BotDecision.ts >= since).order_by(BotDecision.ts.asc()).all() trades = db.query(TradeExecution).filter(TradeExecution.ts >= since).order_by(TradeExecution.ts.asc()).all() # hour buckets buckets = {} for d in decs: k = d.ts.strftime("%m-%d %H:00") buckets.setdefault(k, {"buy": 0, "sell": 0, "hold": 0, "executed": 0, "failed": 0}) if d.action in ("buy", "sell", "hold"): buckets[k][d.action] += 1 if d.status == "executed": buckets[k]["executed"] += 1 if d.status == "failed": buckets[k]["failed"] += 1 labels = list(buckets.keys()) buy = [buckets[k]["buy"] for k in labels] sell = [buckets[k]["sell"] for k in labels] hold = [buckets[k]["hold"] for k in labels] executed = [buckets[k]["executed"] for k in labels] failed = [buckets[k]["failed"] for k in labels] # cumulative notional (proxy activity curve) t_labels, t_values = [], [] c = 0.0 for t in trades: c += float(t.notional or 0) t_labels.append(t.ts.strftime("%m-%d %H:%M")) t_values.append(round(c, 2)) # symbol leaderboard lb = {} for d in decs: row = lb.setdefault(d.symbol, {"symbol": d.symbol, "decisions": 0, "executed": 0}) row["decisions"] += 1 if d.status == "executed": row["executed"] += 1 leaderboard = sorted(lb.values(), key=lambda x: x["executed"], reverse=True) return { "ok": True, "hours": hours, "decisionSeries": { "labels": labels, "buy": buy, "sell": sell, "hold": hold, "executed": executed, "failed": failed, }, "activitySeries": { "labels": t_labels, "cumulativeNotional": t_values, }, "leaderboard": leaderboard, } finally: db.close() @app.get("/") def home(request: Request): db = SessionLocal() try: since = datetime.utcnow() - timedelta(hours=24) decisions = db.query(BotDecision).order_by(BotDecision.ts.desc()).limit(120).all() trades = db.query(TradeExecution).order_by(TradeExecution.ts.desc()).limit(120).all() insights = db.query(CuratedInsight).order_by(CuratedInsight.ts.desc()).limit(30).all() stats = { "decisions": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since).scalar() or 0, "trades": db.query(func.count(TradeExecution.id)).filter(TradeExecution.ts >= since).scalar() or 0, "executed": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since, BotDecision.status == "executed").scalar() or 0, "failed": db.query(func.count(BotDecision.id)).filter(BotDecision.ts >= since, BotDecision.status == "failed").scalar() or 0, "insights": db.query(func.count(CuratedInsight.id)).filter(CuratedInsight.ts >= since).scalar() or 0, } return templates.TemplateResponse("index.html", {"request": request, "decisions": decisions, "trades": trades, "insights": insights, "stats": stats}) finally: db.close()