diff --git a/backend/routers/strategy_builder.py b/backend/routers/strategy_builder.py index 6765fb6..568bcfc 100644 --- a/backend/routers/strategy_builder.py +++ b/backend/routers/strategy_builder.py @@ -282,6 +282,47 @@ def price(req: PriceRequest): result["spot"] = chain_slice["spot"] result["scenario_spot"] = surface_scenario.spot result["proxy"] = chain_slice["proxy"] + + # Debug trace for two open questions (nominal/entry_cost not matching the UI's own bid/ + # ask math, and "-∞" risk on structures that look bounded by hand): log EXACTLY what was + # received and computed, so both can be checked from System Logs (source=strategy_price_debug, + # or filter level=WARNING to jump straight to the unbounded cases) instead of digging + # through the browser's Network tab. Remove once both are confirmed resolved. + from services.database import log_system_event + bounded_risk = result.get("bounded_risk") + log_system_event( + level="INFO" if bounded_risk else "WARNING", + source="strategy_price_debug", + message=( + f"/price {req.scenario.symbol}: {len(legs)} jambe(s), nominal={req.scenario.contract_size}, " + f"entry_cost={result.get('entry_cost')} (mid={result.get('entry_cost_mid')}), " + f"broker_spread_cost={result.get('broker_spread_cost')}, " + f"max_gain={result.get('max_gain')}, max_loss={result.get('max_loss')}, bounded_risk={bounded_risk}" + ), + ticker=req.scenario.symbol, + details={ + "legs_received": legs, + "scenario_received": { + "contract_size": req.scenario.contract_size, + "horizon_days": req.scenario.horizon_days, + "spot_shock_pct": req.scenario.spot_shock_pct, + "iv_level_shift": req.scenario.iv_level_shift, + "skew_tilt": req.scenario.skew_tilt, + "term_slope_shift": req.scenario.term_slope_shift, + "has_spot_path": bool(req.scenario.spot_path), + "has_iv_path": bool(req.scenario.iv_path), + "has_skew_path": bool(req.scenario.skew_path), + "has_term_path": bool(req.scenario.term_path), + }, + "priced": { + "entry_cost": result.get("entry_cost"), "entry_cost_mid": result.get("entry_cost_mid"), + "broker_spread_cost": result.get("broker_spread_cost"), + "max_gain": result.get("max_gain"), "max_loss": result.get("max_loss"), + "bounded_risk": bounded_risk, + }, + "risk_debug": result.get("risk_debug"), + }, + ) return result diff --git a/backend/services/strategy_engine.py b/backend/services/strategy_engine.py index a11eb8a..530e62a 100644 --- a/backend/services/strategy_engine.py +++ b/backend/services/strategy_engine.py @@ -224,6 +224,7 @@ def price_combo( "max_gain": bounded["max_gain"], "max_loss": bounded["max_loss"], "bounded_risk": bounded["bounded"], + "risk_debug": bounded.get("risk_debug"), "greeks_now": greeks_at(legs, spot_now, 0, surface_now, r), "greeks_scenario": greeks_at(legs, spot_scenario, horizon_days, surface_scenario, r), "net_delta_now": delta_now, @@ -281,6 +282,16 @@ 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) + # Diagnostic snapshot of exactly why loss/gain were classified (un)bounded — surfaced + # up through price_combo/payoff_curves so the /price router can log it (system_logs) + # instead of this being a black box every time "-∞" shows up in the UI. + risk_debug = { + "eval_days": eval_days, "spot": spot, "entry_ref": round(entry_ref, 2), "tol": round(tol, 4), + "lo_tail_price": round(float(tail_grid[0]), 6), "lo_edge_pnl": round(lo_edge, 2), "lo_inner_pnl": round(lo_in, 2), + "hi_tail_price": round(float(tail_grid[-1]), 6), "hi_edge_pnl": round(hi_edge, 2), "hi_inner_pnl": round(hi_in, 2), + "loss_bounded": loss_bounded, "gain_bounded": gain_bounded, + } + # 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] @@ -294,6 +305,7 @@ def check_bounded_risk( "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, + "risk_debug": risk_debug, } # Any FIXED grid — however dense — is a different finite sampling of the same @@ -330,6 +342,7 @@ def check_bounded_risk( "bounded": loss_bounded, "max_loss": round(refine(False), 2) if loss_bounded else None, "max_gain": round(refine(True), 2) if gain_bounded else None, + "risk_debug": risk_debug, }