feat: strategy builder

This commit is contained in:
OpenSquared
2026-08-03 11:57:23 +02:00
parent f75dc0e302
commit b73a7388be

View File

@@ -171,18 +171,40 @@ def _resolve_terminal_shocks(scenario: "ScenarioIn"):
) )
def _build_surfaces(scenario: ScenarioIn): def _chain_window_for_legs(scenario: ScenarioIn, legs: Optional[List[Dict[str, Any]]]):
"""get_chain_slice picks the `n_expiries` expiries CLOSEST to horizon_days, even inside
a dte_min/dte_max window — so a calendar/diagonal whose far leg sits well past
horizon_days can silently lose that leg's real quote to the trim (confirmed via the
strategy_price_debug trace: the far leg's exec_price/mid matched a Black-Scholes+5%-
spread FALLBACK price, not its actual bid/ask, because find_quote came up empty against
the narrower chain /price had fetched). When we know the exact legs being priced, widen
the window to guarantee every one of their expiries survives — no reason to rely on a
horizon-proximity heuristic when the expiries are already explicit."""
dte_min, dte_max, n_expiries = scenario.dte_min, scenario.dte_max, scenario.n_expiries
leg_days = [l["days_to_expiry"] for l in (legs or []) if l.get("option_type") != "stock"]
if leg_days:
lo, hi = min(leg_days), max(leg_days)
dte_min = min(dte_min, lo) if dte_min is not None else lo
dte_max = max(dte_max, hi) if dte_max is not None else hi
# Enough slots that narrowing-to-window doesn't get re-trimmed by horizon-proximity
# sort — same n_expiries=20 the /presets endpoint already uses for this reason.
n_expiries = max(n_expiries, 20)
return dte_min, dte_max, n_expiries
def _build_surfaces(scenario: ScenarioIn, legs: Optional[List[Dict[str, Any]]] = None):
dte_min, dte_max, n_expiries = _chain_window_for_legs(scenario, legs)
chain_slice = get_chain_slice( chain_slice = get_chain_slice(
scenario.symbol, scenario.horizon_days, scenario.n_expiries, scenario.symbol, scenario.horizon_days, n_expiries,
dte_min=scenario.dte_min, dte_max=scenario.dte_max, as_of=scenario.as_of, dte_min=dte_min, dte_max=dte_max, as_of=scenario.as_of,
) )
surface_now = build_surface(chain_slice) surface_now = build_surface(chain_slice)
if scenario.checkpoint_as_of: if scenario.checkpoint_as_of:
# Real smile-of-the-day, not a hypothesis — same fitting code as surface_now # Real smile-of-the-day, not a hypothesis — same fitting code as surface_now
# (build_surface), just fed the chain as it stood at the scrubbed-to date. # (build_surface), just fed the chain as it stood at the scrubbed-to date.
checkpoint_chain = get_chain_slice( checkpoint_chain = get_chain_slice(
scenario.symbol, scenario.horizon_days, scenario.n_expiries, scenario.symbol, scenario.horizon_days, n_expiries,
dte_min=scenario.dte_min, dte_max=scenario.dte_max, as_of=scenario.checkpoint_as_of, dte_min=dte_min, dte_max=dte_max, as_of=scenario.checkpoint_as_of,
) )
surface_scenario = build_surface(checkpoint_chain) surface_scenario = build_surface(checkpoint_chain)
else: else:
@@ -255,12 +277,12 @@ def price(req: PriceRequest):
if len(req.legs) > 4: if len(req.legs) > 4:
raise HTTPException(status_code=400, detail="4 jambes maximum") raise HTTPException(status_code=400, detail="4 jambes maximum")
legs = [leg.model_dump() for leg in req.legs]
try: try:
chain_slice, surface_now, surface_scenario = _build_surfaces(req.scenario) chain_slice, surface_now, surface_scenario = _build_surfaces(req.scenario, legs=legs)
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
legs = [leg.model_dump() for leg in req.legs]
# Paths only drive the day-by-day payoff table, and only make sense for the synthetic # Paths only drive the day-by-day payoff table, and only make sense for the synthetic
# parametric scenario — "Analyse période historique" (checkpoint_as_of) prices against # parametric scenario — "Analyse période historique" (checkpoint_as_of) prices against
# a real remembered chain instead, which has no notion of a hypothesized path. # a real remembered chain instead, which has no notion of a hypothesized path.