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>
127 lines
4.5 KiB
Python
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)}
|