from fastapi import APIRouter from pydantic import BaseModel from typing import Optional, List import yfinance as yf import numpy as np import pandas as pd from datetime import datetime from services.options_pricer import black_scholes router = APIRouter(prefix="/api/backtest", tags=["backtest"]) class BacktestRequest(BaseModel): symbol: str start_date: str end_date: str strategy: str # "long_call" | "long_put" | "bull_call_spread" | "bear_put_spread" | "straddle" strike_offset_pct: float = 0.05 # e.g. 5% OTM expiry_days: int = 90 capital: float = 1000.0 geo_filter: Optional[str] = None # optional pattern id to filter @router.post("/run") def run_backtest(req: BacktestRequest): try: ticker = yf.Ticker(req.symbol) hist = ticker.history(start=req.start_date, end=req.end_date, interval="1d") if hist.empty or len(hist) < 20: return {"error": "Insufficient data for the period"} hist = hist.reset_index() returns = np.log(hist["Close"] / hist["Close"].shift(1)).dropna() trades = [] equity = [req.capital] capital = req.capital r = 0.05 T_open = req.expiry_days / 365 step = max(1, req.expiry_days // 3) for i in range(0, len(hist) - req.expiry_days, step): row = hist.iloc[i] S = float(row["Close"]) date_str = str(row["Date"])[:10] sigma_window = returns.iloc[max(0, i - 30):i] if len(sigma_window) < 5: continue sigma = float(sigma_window.std() * np.sqrt(252)) if sigma < 0.01: sigma = 0.20 if req.strategy in ["long_call", "bull_call_spread"]: K = S * (1 + req.strike_offset_pct) else: K = S * (1 - req.strike_offset_pct) result = black_scholes(S, K, T_open, r, sigma, "call" if "call" in req.strategy else "put") premium = result["price"] contracts = max(1, int((capital * 0.1) / (premium * 100))) cost = contracts * premium * 100 expiry_idx = min(i + req.expiry_days, len(hist) - 1) S_expiry = float(hist.iloc[expiry_idx]["Close"]) date_expiry = str(hist.iloc[expiry_idx]["Date"])[:10] if req.strategy in ["long_call", "bull_call_spread"]: intrinsic = max(0, S_expiry - K) else: intrinsic = max(0, K - S_expiry) pnl = (intrinsic - premium) * contracts * 100 capital += pnl equity.append(round(capital, 2)) trades.append({ "entry_date": date_str, "exit_date": date_expiry, "strategy": req.strategy, "S_entry": round(S, 2), "K": round(K, 2), "premium": round(premium, 4), "contracts": contracts, "cost": round(cost, 2), "S_expiry": round(S_expiry, 2), "intrinsic": round(intrinsic, 4), "pnl": round(pnl, 2), "capital": round(capital, 2), }) if not trades: return {"error": "No trades generated"} wins = [t for t in trades if t["pnl"] > 0] losses = [t for t in trades if t["pnl"] <= 0] total_pnl = sum(t["pnl"] for t in trades) gross_profit = sum(t["pnl"] for t in wins) if wins else 0 gross_loss = abs(sum(t["pnl"] for t in losses)) if losses else 1 eq = np.array(equity) peak = np.maximum.accumulate(eq) drawdown = (eq - peak) / peak max_dd = float(drawdown.min()) * 100 equity_curve = [{"index": i, "capital": v} for i, v in enumerate(equity)] return { "symbol": req.symbol, "strategy": req.strategy, "period": f"{req.start_date} → {req.end_date}", "total_trades": len(trades), "wins": len(wins), "losses": len(losses), "win_rate": round(len(wins) / len(trades) * 100, 1) if trades else 0, "total_pnl": round(total_pnl, 2), "total_return_pct": round((capital - req.capital) / req.capital * 100, 2), "max_drawdown_pct": round(max_dd, 2), "profit_factor": round(gross_profit / gross_loss, 2) if gross_loss else 0, "final_capital": round(capital, 2), "equity_curve": equity_curve, "trades": trades[-20:], } except Exception as e: return {"error": str(e)}