feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-19 09:39:09 +02:00
parent e7247d4c4c
commit 3417bb6075
6 changed files with 142 additions and 62 deletions

View File

@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional
from services.option_chain import get_chain_slice
from services.vol_surface import Surface, ScenarioSurface, build_surface, apply_scenario
from services.strategy_engine import price_combo, expected_pnl_scenario, to_native
from services.strategy_engine import price_combo, expected_pnl_scenario, to_native, DEFAULT_CONTRACT_SIZE
from services.strategy_templates import generate_all, strikes_for
MAX_SEEDS_FOR_RESIDUAL_SEARCH = 40
@@ -18,7 +18,7 @@ RESIDUAL_ITERATIONS_PER_SEED = 8
RESIDUAL_MAX_EVALS = 400
def _score(priced: Dict[str, Any], legs: List[Dict[str, Any]], objective: str, surface_scenario: ScenarioSurface, horizon_days: int, r: float) -> Optional[float]:
def _score(priced: Dict[str, Any], legs: List[Dict[str, Any]], objective: str, surface_scenario: ScenarioSurface, horizon_days: int, r: float, contract_size: float) -> Optional[float]:
if objective == "net_pnl":
return priced["net_pnl"]
if objective == "return_on_risk":
@@ -26,7 +26,7 @@ def _score(priced: Dict[str, Any], legs: List[Dict[str, Any]], objective: str, s
return None
return priced["net_pnl"] / abs(priced["max_loss"])
if objective == "prob_weighted":
return expected_pnl_scenario(legs, surface_scenario, horizon_days, r, priced["entry_cost"])
return expected_pnl_scenario(legs, surface_scenario, horizon_days, r, priced["entry_cost"], contract_size=contract_size)
raise ValueError(f"Objectif inconnu: {objective}")
@@ -46,16 +46,17 @@ def _passes_constraints(legs: List[Dict[str, Any]], priced: Dict[str, Any], cons
def _evaluate(
name: str, legs: List[Dict[str, Any]], chain_slice: Dict[str, Any], surface_now: Surface,
surface_scenario: ScenarioSurface, horizon_days: int, r: float, constraints: Dict[str, Any], objective: str,
contract_size: float = DEFAULT_CONTRACT_SIZE,
) -> Optional[Dict[str, Any]]:
if len(legs) > constraints["max_legs"] or len(legs) == 0:
return None
try:
priced = price_combo(legs, chain_slice, surface_now, surface_scenario, horizon_days, r)
priced = price_combo(legs, chain_slice, surface_now, surface_scenario, horizon_days, r, contract_size)
except Exception:
return None
if not _passes_constraints(legs, priced, constraints):
return None
score = _score(priced, legs, objective, surface_scenario, horizon_days, r)
score = _score(priced, legs, objective, surface_scenario, horizon_days, r, contract_size)
if score is None:
return None
return {"template_name": name, "legs": legs, "score": round(score, 2), "objective": objective, **priced}
@@ -87,6 +88,7 @@ def _perturb(legs: List[Dict[str, Any]], strikes_by_expiry: Dict[Any, List[float
def _residual_search(
seeds: List[Dict[str, Any]], chain_slice: Dict[str, Any], surface_now: Surface, surface_scenario: ScenarioSurface,
horizon_days: int, r: float, constraints: Dict[str, Any], objective: str,
contract_size: float = DEFAULT_CONTRACT_SIZE,
) -> List[Dict[str, Any]]:
strikes_by_expiry = {
(exp["expiry_date"], opt_type): strikes_for(exp, opt_type)
@@ -104,7 +106,7 @@ def _residual_search(
evals += 1
evaluated = _evaluate(
f"{seed['template_name']} (variante)", candidate_legs, chain_slice, surface_now,
surface_scenario, horizon_days, r, constraints, objective,
surface_scenario, horizon_days, r, constraints, objective, contract_size,
)
if evaluated and evaluated["score"] > current["score"]:
current = evaluated
@@ -146,6 +148,7 @@ def optimize(
constraints: Dict[str, Any],
objective: str,
top_n: int = 20,
contract_size: float = DEFAULT_CONTRACT_SIZE,
) -> List[Dict[str, Any]]:
chain_slice = get_chain_slice(symbol, horizon_days, n_expiries)
surface_now = build_surface(chain_slice)
@@ -157,14 +160,14 @@ def optimize(
candidates = generate_all(chain_slice)
scored: List[Dict[str, Any]] = []
for name, legs in candidates:
evaluated = _evaluate(name, legs, chain_slice, surface_now, surface_scenario, horizon_days, rate, constraints, objective)
evaluated = _evaluate(name, legs, chain_slice, surface_now, surface_scenario, horizon_days, rate, constraints, objective, contract_size)
if evaluated:
scored.append(evaluated)
scored.sort(key=lambda c: c["score"], reverse=True)
seeds = scored[:MAX_SEEDS_FOR_RESIDUAL_SEARCH]
refined = _residual_search(seeds, chain_slice, surface_now, surface_scenario, horizon_days, rate, constraints, objective)
refined = _residual_search(seeds, chain_slice, surface_now, surface_scenario, horizon_days, rate, constraints, objective, contract_size)
scored.extend(refined)
scored.sort(key=lambda c: c["score"], reverse=True)