""" FastAPI backend for the TBS Straddle Backtest Dashboard. Serves both the API endpoints and the static frontend files. """ import os import sys import traceback from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pydantic import BaseModel from engine import BacktestEngine, MONEYNESS, SL_LEVELS # ─────────────────────────── DATA ───────────────────────────── HERE = os.path.dirname(os.path.abspath(__file__)) DATA_PATH = os.environ.get( "DATA_PATH", os.path.join(HERE, "nifty_minute_chain.parquet"), ) FRONTEND_DIR = os.path.join(HERE, "frontend") engine = None # loaded in lifespan @asynccontextmanager async def lifespan(app: FastAPI): global engine print(f"[startup] Loading data from {DATA_PATH} …", flush=True) try: engine = BacktestEngine(DATA_PATH) print(f"[startup] Engine ready — {engine.data_info['expiry_days']} expiry days loaded.", flush=True) except Exception as e: print(f"[startup] FATAL: {e}", flush=True) traceback.print_exc() sys.exit(1) yield # ─────────────────────────── APP ────────────────────────────── app = FastAPI(title="TBS Straddle Backtest Engine", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) # ─────────────────────────── MODELS ─────────────────────────── class BacktestRequest(BaseModel): moneyness: str = "ATM" min_straddle: float = 0.0 class BasketRequest(BaseModel): moneyness: str = "ATM" min_straddle: float = 0.0 basket_type: str = "15min" # ─────────────────────────── API ────────────────────────────── @app.get("/api/info") def get_info(): return { "data": engine.data_info, "moneyness_options": list(MONEYNESS.keys()), "moneyness_details": MONEYNESS, "sl_levels": SL_LEVELS, "panel_days": len(engine.PANEL), } @app.get("/api/opening-straddle") def get_opening_straddle(): return engine.get_opening_straddle() @app.post("/api/backtest/single") def run_single(req: BacktestRequest): if req.moneyness not in MONEYNESS: raise HTTPException(400, f"Unknown moneyness: {req.moneyness}") return engine.run_single_entry(req.moneyness, req.min_straddle) @app.post("/api/backtest/baskets") def run_baskets(req: BasketRequest): if req.moneyness not in MONEYNESS: raise HTTPException(400, f"Unknown moneyness: {req.moneyness}") if req.basket_type not in ('15min', '2hr'): raise HTTPException(400, "basket_type must be '15min' or '2hr'") return engine.run_basket_backtest(req.moneyness, req.min_straddle, req.basket_type) # ─────────────── SERVE FRONTEND (static files) ─────────────── if os.path.isdir(FRONTEND_DIR): app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static") @app.get("/") def serve_index(): return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))