feat: strategy builder

This commit is contained in:
OpenSquared
2026-07-19 10:55:56 +02:00
parent 5d8bfa3371
commit 67952e01fa
5 changed files with 35 additions and 26 deletions

View File

@@ -44,7 +44,7 @@ class PriceRequest(BaseModel):
class ConstraintsIn(BaseModel):
max_legs: int = 4
delta_threshold: float = 0.15
delta_threshold: Optional[float] = 0.15
max_loss_cap: Optional[float] = None
objective: str = "net_pnl" # "net_pnl" | "return_on_risk" | "prob_weighted"
top_n: int = 20

View File

@@ -197,18 +197,24 @@ def check_bounded_risk(
calendar/diagonal spread it's only the near leg — the far leg is still alive and needs
`surface` to be priced, so max_gain/max_loss there is only as good as that vol input.
`precise=False` skips the dense near-strike grid and the 1-D refinement below (bounded-
ness itself is unaffected — it only needs the tail behavior). The optimizer's bulk scan
(hundreds of candidates) uses this fast path since ranking only needs relative ordering;
the single-candidate /price call uses the full precise path.
`precise=False` skips only the 1-D refinement below (the expensive part — two
minimize_scalar calls). The dense near-strike grid stays even in the fast path: a long
straddle's max loss (or a butterfly's max gain) sits exactly AT a strike too, same as a
calendar spread's peak, and the wide log-spaced tail grid alone is coarse enough near
the money to miss it by an order of magnitude, not just a rounding difference (confirmed:
on a real long straddle, tail-grid-only reported max_loss=-117 when the true V-bottom at
the strike was -1140 — nearly 10x off, and wrong enough to silently let a candidate past
a max_loss_cap filter it should have failed). The optimizer's bulk scan (hundreds of
candidates) uses this fast path for speed; the single-candidate /price call refines
further with minimize_scalar for pixel-perfect precision.
"""
eval_days = min(l["days_to_expiry"] for l in legs)
def f(s: float) -> float:
return value_at(legs, s, eval_days, surface, r, contract_size) - entry_ref
# Wide, log-spaced tail grid — used only to detect whether the payoff flattens out
# (bounded) toward either extreme, or keeps moving further away.
# Wide, log-spaced tail grid — used to detect whether the payoff flattens out (bounded)
# toward either extreme, or keeps moving further away.
tail_grid = np.geomspace(spot * 0.05, spot * 20, 300)
tail_values = [f(float(p)) for p in tail_grid]
@@ -220,25 +226,26 @@ def check_bounded_risk(
loss_bounded = (lo_edge >= lo_in - tol) and (hi_edge >= hi_in - tol)
gain_bounded = (lo_edge <= lo_in + tol) and (hi_edge <= hi_in + tol)
if not precise:
return {
"bounded": loss_bounded,
"max_loss": round(min(tail_values), 2) if loss_bounded else None,
"max_gain": round(max(tail_values), 2) if gain_bounded else None,
}
# A calendar spread's (or ratio spread's) real best/worst case is a sharp peak right at
# a strike, not out in the tails. Any FIXED grid — however dense — is a different finite
# sampling of the same continuous curve than whatever grid a chart or another caller
# uses, so two "close but not identical" readings of the same peak are pretty much
# guaranteed (confirmed: this grid vs the payoff chart's own grid gave two different
# peak heights for the same trade). Add a dense linear sweep across the legs' own
# strikes to locate the right neighborhood...
# Dense linear sweep across the legs' own strikes — needed even in the fast path (see
# docstring above), only the final 1-D refinement is reserved for precise=True.
strikes = [l["strike"] for l in legs]
lo_k, hi_k = min(strikes) * 0.7, max(strikes) * 1.3
near_grid = np.linspace(max(lo_k, spot * 0.05), min(hi_k, spot * 20), 400)
near_values = [f(float(p)) for p in near_grid]
combined_values = tail_values + near_values
if not precise:
return {
"bounded": loss_bounded,
"max_loss": round(min(combined_values), 2) if loss_bounded else None,
"max_gain": round(max(combined_values), 2) if gain_bounded else None,
}
# Any FIXED grid — however dense — is a different finite sampling of the same
# continuous curve than whatever grid a chart or another caller uses, so two "close but
# not identical" readings of the same peak are pretty much guaranteed (confirmed: this
# grid vs the payoff chart's own grid gave two different peak heights for the same
# trade). Refine with a bounded 1-D optimizer around the grid's own best point instead.
grid_all = np.concatenate([tail_grid, near_grid])
values_all = np.concatenate([tail_values, near_values])
order = np.argsort(grid_all)

View File

@@ -33,7 +33,8 @@ def _score(priced: Dict[str, Any], legs: List[Dict[str, Any]], objective: str, s
def _passes_constraints(legs: List[Dict[str, Any]], priced: Dict[str, Any], constraints: Dict[str, Any]) -> bool:
if len(legs) > constraints["max_legs"]:
return False
if abs(priced["net_delta_now"]) > constraints["delta_threshold"]:
delta_threshold = constraints.get("delta_threshold")
if delta_threshold is not None and abs(priced["net_delta_now"]) > delta_threshold:
return False
if not priced["bounded_risk"]:
return False