feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-31 13:56:49 +02:00
parent cc22cbd3e0
commit a94f783d33
3 changed files with 117 additions and 29 deletions

View File

@@ -398,16 +398,59 @@ def time_decay_slices(
return slices
def _find_breakevens(
legs: List[Dict[str, Any]], surface: Any, eval_days_expiry: float, r: float,
entry_ref: float, spot: float, contract_size: float, n: int = 300,
) -> List[float]:
"""Exact expiry P&L zero-crossings — the same value_at boundary check_bounded_risk
already prices, scanned for sign changes and bisected instead of searched for its
extrema. Lets payoff_heatmap pin a real breakeven column instead of only ever landing
near one by luck of the price sampling."""
def f(s: float) -> float:
return value_at(legs, s, eval_days_expiry, surface, r, contract_size) - entry_ref
grid = np.linspace(max(spot * 0.2, 1e-6), spot * 3.0, n)
vals = [f(float(p)) for p in grid]
roots: List[float] = []
for i in range(len(grid) - 1):
a, b = vals[i], vals[i + 1]
if a == 0:
roots.append(float(grid[i]))
continue
if (a < 0) != (b < 0):
lo_b, hi_b, f_lo = float(grid[i]), float(grid[i + 1]), a
for _ in range(30):
mid = (lo_b + hi_b) / 2
f_mid = f(mid)
if (f_mid < 0) == (f_lo < 0):
lo_b, f_lo = mid, f_mid
else:
hi_b = mid
roots.append(round((lo_b + hi_b) / 2, 4))
return roots
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,
entry_ref: float, contract_size: float = DEFAULT_CONTRACT_SIZE, n_prices: int = 17, 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)
to expiry (top-to-bottom reading matches watching the position age). Columns are
centered and symmetric around spot, scaled to how far the legs' own strikes sit from it
(tight for a near-the-money single leg, wide for a far-strike spread) — a fixed +-15%
window left more than half the grid flat at max loss/gain for a near-the-money position,
wasting resolution nowhere near where the P&L actually transitions. The exact expiry
breakeven(s) are pinned in as extra columns (breakeven_prices in the response) instead
of only ever landing near one by luck of the price sampling."""
strikes = [l["strike"] for l in legs if l["option_type"] != "stock"]
half_width = max(max(abs(spot - k) for k in strikes) * 1.4, spot * 0.03) if strikes else spot * 0.15
lo, hi = max(spot - half_width, spot * 0.01), spot + half_width
breakevens = _find_breakevens(legs, surface, eval_days_expiry, r, entry_ref, spot, contract_size)
near_breakevens = sorted((p for p in breakevens if lo <= p <= hi), key=lambda p: abs(p - spot))[:2]
price_points = np.unique(np.concatenate([np.linspace(lo, hi, n_prices), np.array(near_breakevens)]))
price_points.sort()
day_points = np.linspace(0, eval_days_expiry, n_days)
rows = [
{
@@ -416,7 +459,11 @@ def payoff_heatmap(
}
for d in day_points
]
return {"prices": [round(float(p), 4) for p in price_points], "rows": rows}
return {
"prices": [round(float(p), 4) for p in price_points],
"rows": rows,
"breakeven_prices": [round(p, 4) for p in near_breakevens],
}
def payoff_curves(