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