Files
OpenFin/backend/routers/backtest.py
OpenSquared d256b65d30 Initial commit — GeoOptions Intelligence Cockpit v2.0
Stack: FastAPI + React/TypeScript + SQLite + GPT-4o
Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM,
Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA.
Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:29:59 +02:00

127 lines
4.5 KiB
Python

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)}