feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-31 12:12:24 +02:00
parent 49ebd75522
commit cc22cbd3e0
6 changed files with 316 additions and 246 deletions

View File

@@ -41,12 +41,21 @@ class ScenarioIn(BaseModel):
# options (e.g. dte_min=20, dte_max=60) instead of horizon_days doing double duty.
dte_min: Optional[int] = None
dte_max: Optional[int] = None
# "Dériver d'un historique" mode: reconstruct the chain (and every leg strike drawn
# "Analyse période historique" mode: reconstruct the chain (and every leg strike drawn
# from it, including in /optimize) as it stood at/before this date instead of live —
# e.g. so the optimizer searches over what was ACTUALLY quoted on the day a realized
# scenario's window starts, not today's chain. None (default) = live, unchanged
# behavior for the normal Construire flow.
# behavior for the normal Construire flow. This is the position's ENTRY date.
as_of: Optional[str] = None
# The scrubbed-to day within the historical period: when set, surface_scenario is a
# REAL smile fit from the Saxo chain at/before this date (services.vol_surface.Surface,
# built the same way surface_now already is) instead of apply_scenario's parametric
# spot/IV/skew/term shock — Surface and ScenarioSurface expose the same .spot/.iv_at()
# interface, so this is a drop-in substitution, not a new pricing path. horizon_days
# should equal (checkpoint_as_of - as_of).days so the elapsed-time math stays
# consistent with what's actually being priced. None (default) = today's synthetic
# scenario shock, unchanged behavior.
checkpoint_as_of: Optional[str] = None
@property
def shocked_rate(self) -> float:
@@ -119,6 +128,7 @@ class StrategySaveRequest(BaseModel):
net_pnl_scenario: Optional[float] = None
net_delta: Optional[float] = None
notes: Optional[str] = ""
source: str = "synthetic" # "synthetic" (Construire) | "historical" (Analyse période historique)
def _build_surfaces(scenario: ScenarioIn):
@@ -127,14 +137,23 @@ def _build_surfaces(scenario: ScenarioIn):
dte_min=scenario.dte_min, dte_max=scenario.dte_max, as_of=scenario.as_of,
)
surface_now = build_surface(chain_slice)
surface_scenario = apply_scenario(
surface_now,
spot_shock_pct=scenario.spot_shock_pct,
iv_level_shift=scenario.iv_level_shift,
skew_tilt=scenario.skew_tilt,
term_slope_shift=scenario.term_slope_shift,
manual_grid=scenario.manual_grid,
)
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,
)
surface_scenario = build_surface(checkpoint_chain)
else:
surface_scenario = apply_scenario(
surface_now,
spot_shock_pct=scenario.spot_shock_pct,
iv_level_shift=scenario.iv_level_shift,
skew_tilt=scenario.skew_tilt,
term_slope_shift=scenario.term_slope_shift,
manual_grid=scenario.manual_grid,
)
return chain_slice, surface_now, surface_scenario
@@ -287,6 +306,8 @@ def optimize(req: OptimizeRequest):
top_n=req.constraints.top_n,
contract_size=req.scenario.contract_size,
greek_profile=req.greek_profile.model_dump() if req.greek_profile else None,
as_of=req.scenario.as_of,
checkpoint_as_of=req.scenario.checkpoint_as_of,
)
except Exception as e:
import traceback