120 lines
4.5 KiB
Python
120 lines
4.5 KiB
Python
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, qdrant_memory_health
|
|
|
|
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,
|
|
"qdrant": qdrant_memory_health(),
|
|
}
|
|
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()
|