feat: strategy builder
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user