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

@@ -341,6 +341,11 @@ def init_db():
"ALTER TABLE strategy_scenarios ADD COLUMN rate_shock_bps REAL DEFAULT 0",
"ALTER TABLE strategy_scenarios ADD COLUMN dte_min INTEGER",
"ALTER TABLE strategy_scenarios ADD COLUMN dte_max INTEGER",
# Strategy Builder — Construire/Analyse historique merge: tags which mode priced
# this strategy when it was saved, so the saved-strategies library (now shown in
# both modes) can badge it and the other mode knows it's re-pricing a strategy that
# wasn't originally priced there.
"ALTER TABLE saved_strategies ADD COLUMN source TEXT DEFAULT 'synthetic'",
]:
try:
c.execute(_sql)
@@ -6421,8 +6426,8 @@ def save_strategy(strategy: Dict[str, Any]) -> str:
conn = get_conn()
conn.execute("""INSERT INTO saved_strategies (
id, scenario_id, symbol, template_name, objective, legs,
entry_cost, max_gain, max_loss, net_pnl_scenario, net_delta, notes
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", (
entry_cost, max_gain, max_loss, net_pnl_scenario, net_delta, notes, source
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""", (
strategy_id,
strategy.get("scenario_id"),
strategy["symbol"],
@@ -6435,6 +6440,7 @@ def save_strategy(strategy: Dict[str, Any]) -> str:
strategy.get("net_pnl_scenario"),
strategy.get("net_delta"),
strategy.get("notes", ""),
strategy.get("source", "synthetic"),
))
conn.commit()
conn.close()

View File

@@ -274,14 +274,23 @@ def optimize(
dte_max: Optional[int] = None,
greek_profile: Optional[Dict[str, Any]] = None,
as_of: Optional[str] = None,
checkpoint_as_of: Optional[str] = None,
) -> List[Dict[str, Any]]:
r = rate + rate_shock_bps / 10000.0
chain_slice = get_chain_slice(symbol, horizon_days, n_expiries, dte_min=dte_min, dte_max=dte_max, as_of=as_of)
surface_now = build_surface(chain_slice)
surface_scenario = apply_scenario(
surface_now, spot_shock_pct=spot_shock_pct, iv_level_shift=iv_level_shift,
skew_tilt=skew_tilt, term_slope_shift=term_slope_shift, manual_grid=manual_grid,
)
if checkpoint_as_of:
# Real smile-of-the-day (same fitting as surface_now) instead of a parametric
# shock — see routers.strategy_builder.ScenarioIn.checkpoint_as_of. Surface and
# ScenarioSurface share the same .spot/.iv_at() interface, so every candidate
# evaluated below (_evaluate/_residual_search) needs no change.
checkpoint_chain = get_chain_slice(symbol, horizon_days, n_expiries, dte_min=dte_min, dte_max=dte_max, as_of=checkpoint_as_of)
surface_scenario = build_surface(checkpoint_chain)
else:
surface_scenario = apply_scenario(
surface_now, spot_shock_pct=spot_shock_pct, iv_level_shift=iv_level_shift,
skew_tilt=skew_tilt, term_slope_shift=term_slope_shift, manual_grid=manual_grid,
)
candidates = generate_all(chain_slice)
scored: List[Dict[str, Any]] = []

View File

@@ -118,6 +118,10 @@ def replay_position(
"date": d, "spot": chain.get("spot"),
"position_value": round(value, 2),
"pnl": round(value - entry_value, 2),
# Every day's real per-leg quote/IV/greeks, not just entry/exit — lets the
# "Analyse période historique" day-scrubber show the real leg detail for
# whichever day is currently scrubbed to, not only the window's endpoints.
"legs": day_legs,
})
if not points: