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(
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user