feat: strategy builder
This commit is contained in:
@@ -56,7 +56,10 @@ def _settle_leg(leg: BacktestLeg, strike: float, days_to_expiry: int, near_days:
|
||||
"""Value one leg at the near expiry: intrinsic if it expires there too (the common
|
||||
case), else a fresh Black-Scholes price for its remaining time (a 'far' leg — closed
|
||||
alongside the near leg rather than held to its own later expiry, the standard way
|
||||
calendar/diagonal-style structures are actually managed)."""
|
||||
calendar/diagonal-style structures are actually managed). A 'stock' leg (Covered
|
||||
Call/Protective Put/Collar's underlying position) is worth exactly the spot, always."""
|
||||
if leg.option_type == "stock":
|
||||
return S_settle
|
||||
remaining_days = days_to_expiry - near_days
|
||||
if remaining_days <= 0:
|
||||
if leg.option_type == "call":
|
||||
@@ -70,7 +73,7 @@ def _settle_leg(leg: BacktestLeg, strike: float, days_to_expiry: int, near_days:
|
||||
def run_backtest(req: BacktestRequest):
|
||||
try:
|
||||
for leg in req.legs:
|
||||
if leg.option_type not in ("call", "put") or leg.position not in ("long", "short"):
|
||||
if leg.option_type not in ("call", "put", "stock") or leg.position not in ("long", "short"):
|
||||
return {"error": f"Jambe invalide: {leg}"}
|
||||
|
||||
ticker = yf.Ticker(req.symbol)
|
||||
@@ -103,7 +106,7 @@ def run_backtest(req: BacktestRequest):
|
||||
leg_days = [req.expiry_days if leg.expiry != "far" else req.far_expiry_days for leg in req.legs]
|
||||
|
||||
entry_premiums = [
|
||||
float(black_scholes(S, k, d / 365, r, sigma, leg.option_type)["price"])
|
||||
S if leg.option_type == "stock" else float(black_scholes(S, k, d / 365, r, sigma, leg.option_type)["price"])
|
||||
for leg, k, d in zip(req.legs, leg_strikes, leg_days)
|
||||
]
|
||||
|
||||
|
||||
@@ -146,6 +146,38 @@ def chain(
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/presets")
|
||||
def presets(
|
||||
symbol: str = Query(...),
|
||||
horizon_days: int = Query(8),
|
||||
dte_min: Optional[int] = Query(None),
|
||||
dte_max: Optional[int] = Query(None),
|
||||
):
|
||||
"""The full strategy catalog (services.backtest_strategies.STRATEGIES) built from the
|
||||
REAL current chain instead of Backtest's synthetic grid — so a preset click here seeds
|
||||
the leg editor with actually-quoted strikes/expiries, ready to price or replay as-is.
|
||||
n_expiries=5 (vs. Strategy Builder's own default of 3) so calendar/diagonal presets,
|
||||
which need two distinct expiries, reliably have a second one to draw from."""
|
||||
from services.backtest_strategies import STRATEGIES, build_legs
|
||||
try:
|
||||
chain_slice = get_chain_slice(symbol, horizon_days, 5, dte_min=dte_min, dte_max=dte_max)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
expiries = chain_slice["expiries"]
|
||||
if not expiries:
|
||||
raise HTTPException(status_code=404, detail=f"Aucune échéance exploitable pour '{symbol}'.")
|
||||
near, far = expiries[0], expiries[1] if len(expiries) > 1 else None
|
||||
|
||||
out = []
|
||||
for key, label, n_legs in STRATEGIES:
|
||||
legs = build_legs(key, chain_slice["spot"], 0.05, near, far)
|
||||
if not legs:
|
||||
continue # e.g. calendar/diagonal with only one real expiry available right now
|
||||
out.append({"key": key, "label": label, "n_legs": n_legs, "legs": legs})
|
||||
return out
|
||||
|
||||
|
||||
@router.post("/price")
|
||||
def price(req: PriceRequest):
|
||||
if not req.legs:
|
||||
|
||||
Reference in New Issue
Block a user