feat: strategy builder
This commit is contained in:
@@ -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(
|
||||
scenario.symbol, scenario.horizon_days, scenario.n_expiries,
|
||||
dte_min=scenario.dte_min, dte_max=scenario.dte_max, as_of=scenario.as_of,
|
||||
scenario.symbol, scenario.horizon_days, n_expiries,
|
||||
dte_min=dte_min, dte_max=dte_max, as_of=scenario.as_of,
|
||||
)
|
||||
surface_now = build_surface(chain_slice)
|
||||
if scenario.checkpoint_as_of:
|
||||
# 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.
|
||||
checkpoint_chain = get_chain_slice(
|
||||
scenario.symbol, scenario.horizon_days, scenario.n_expiries,
|
||||
dte_min=scenario.dte_min, dte_max=scenario.dte_max, as_of=scenario.checkpoint_as_of,
|
||||
scenario.symbol, scenario.horizon_days, n_expiries,
|
||||
dte_min=dte_min, dte_max=dte_max, as_of=scenario.checkpoint_as_of,
|
||||
)
|
||||
surface_scenario = build_surface(checkpoint_chain)
|
||||
else:
|
||||
@@ -255,12 +277,12 @@ def price(req: PriceRequest):
|
||||
if len(req.legs) > 4:
|
||||
raise HTTPException(status_code=400, detail="4 jambes maximum")
|
||||
|
||||
legs = [leg.model_dump() for leg in req.legs]
|
||||
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:
|
||||
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
|
||||
# parametric scenario — "Analyse période historique" (checkpoint_as_of) prices against
|
||||
# a real remembered chain instead, which has no notion of a hypothesized path.
|
||||
|
||||
Reference in New Issue
Block a user