feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-31 09:32:33 +02:00
parent bef0092ca8
commit c7dc8b5cdc
3 changed files with 179 additions and 10 deletions

View File

@@ -375,6 +375,50 @@ def expected_pnl_scenario(
return float(numerator / denominator) if denominator > 1e-12 else float(pnl.mean())
def time_decay_slices(
legs: List[Dict[str, Any]], prices: np.ndarray, surface: Any, eval_days_expiry: float, r: float,
entry_ref: float, contract_size: float = DEFAULT_CONTRACT_SIZE, n_slices: int = 4,
) -> List[Dict[str, Any]]:
"""Payoff curve at n_slices evenly-spaced elapsed-day checkpoints between today (0) and
the nearest leg's expiry — the "T+0/T+10/T+20..." view that shows how the curve morphs
from today's time-value-laden shape into the kinked expiry payoff, instead of only the
two endpoints at_expiry/at_scenario give. Uses the same scenario vol view as those two
curves (see payoff_curves' own comment) so the only thing that varies between slices is
time decay, not the vol assumption."""
day_points = np.linspace(0, eval_days_expiry, n_slices)
slices = []
for d in day_points:
d = float(d)
label = "Aujourd'hui" if d < 0.5 else ("Échéance" if d >= eval_days_expiry - 0.5 else f"J+{round(d)}")
points = [
{"underlying": round(float(p), 4), "pnl": round(float(value_at(legs, float(p), d, surface, r, contract_size) - entry_ref), 2)}
for p in prices
]
slices.append({"days_from_now": round(d, 1), "label": label, "points": points})
return slices
def payoff_heatmap(
legs: List[Dict[str, Any]], surface: Any, eval_days_expiry: float, r: float, spot: float,
entry_ref: float, contract_size: float = DEFAULT_CONTRACT_SIZE, n_prices: int = 9, n_days: int = 7,
) -> Dict[str, Any]:
"""Price x days-to-expiry grid of P&L — rows are elapsed-day checkpoints from today down
to expiry (top-to-bottom reading matches watching the position age), columns are
underlying prices zoomed closer to spot than the line chart (a heatmap only reads well
over the range where the color actually varies)."""
lo, hi = spot * 0.85, spot * 1.15
price_points = np.linspace(lo, hi, n_prices)
day_points = np.linspace(0, eval_days_expiry, n_days)
rows = [
{
"days_from_now": round(float(d), 1),
"pnl": [round(float(value_at(legs, float(p), float(d), surface, r, contract_size) - entry_ref), 2) for p in price_points],
}
for d in day_points
]
return {"prices": [round(float(p), 4) for p in price_points], "rows": rows}
def payoff_curves(
legs: List[Dict[str, Any]],
chain_slice: Dict[str, Any],
@@ -421,4 +465,13 @@ def payoff_curves(
{"underlying": round(float(p), 4), "pnl": round(float(value_at(legs, float(p), horizon_days, surface_scenario, r, contract_size) - entry_ref), 2)}
for p in prices
]
return {"at_expiry": at_expiry, "at_scenario": at_scenario, **priced}
# Coarser price grid than the two headline curves above — this trades some precision
# for keeping a single /price request's added cost bounded (n_slices/heatmap cells x
# their own price points, on top of the ~1400 value_at calls at_expiry/at_scenario
# above already need).
slice_prices = np.linspace(lo, hi, 80)
time_slices = time_decay_slices(legs, slice_prices, surface_scenario, eval_days_expiry, r, entry_ref, contract_size)
heatmap = payoff_heatmap(legs, surface_scenario, eval_days_expiry, r, spot, entry_ref, contract_size)
return {"at_expiry": at_expiry, "at_scenario": at_scenario, "time_slices": time_slices, "heatmap": heatmap, **priced}