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