import numpy as np from scipy.stats import norm from typing import Dict, Any, List, Optional from datetime import datetime, timedelta import math def black_scholes( S: float, K: float, T: float, r: float, sigma: float, option_type: str = "call", include_second_order: bool = True, ) -> Dict[str, float]: """Black-Scholes pricing + Greeks (first-order delta/gamma/theta/vega/rho, plus the second-order Greeks used by Strategy Builder's "advanced sensitivities" panel: vanna, charm, vomma/volga, veta, speed, color, zomma — vera deliberately omitted, see project memory "Strategy Builder Greeks plan"). All second-order values are scaled to match the convention their related first-order Greek already uses here — e.g. vanna/vomma/zomma are "per vol POINT" like vega already is (not per unit of raw decimal sigma), charm/ color/veta are "per DAY" like theta already is (not per year) — every formula/scaling is verified against finite-difference bumps of this same function's own first-order outputs (see scratchpad test_second_order_greeks.py from the Phase 3 build), not just hand-derived from a textbook, since these third-derivative formulas are easy to get subtly wrong. `include_second_order=False` skips that block entirely — strategy_engine.value_at() (the workhorse of check_bounded_risk's ~700-point grid search per candidate, itself called for every candidate the optimizer scans) only ever reads `["price"]`, so paying for 7 unused derivatives on every one of those hundreds of thousands of calls was pure waste discovered while profiling the Phase 4 retrospective-comparison feature — this flag is what fixed it, not a hypothetical optimization.""" S = float(S or 100.0) K = float(K or S) T = float(T or 0.001) sigma = float(sigma or 0.25) if T <= 0 or sigma <= 0: intrinsic = max(0, S - K) if option_type == "call" else max(0, K - S) result = {"price": intrinsic, "delta": 0, "gamma": 0, "theta": 0, "vega": 0, "rho": 0} if include_second_order: result.update({"vanna": 0, "charm": 0, "vomma": 0, "veta": 0, "speed": 0, "color": 0, "zomma": 0}) return result sqrtT = math.sqrt(T) d1 = (math.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * sqrtT) d2 = d1 - sigma * sqrtT phi_d1 = norm.pdf(d1) if option_type == "call": price = S * norm.cdf(d1) - K * math.exp(-r * T) * norm.cdf(d2) delta = norm.cdf(d1) rho = K * T * math.exp(-r * T) * norm.cdf(d2) / 100 else: price = K * math.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1) delta = norm.cdf(d1) - 1 rho = -K * T * math.exp(-r * T) * norm.cdf(-d2) / 100 gamma = phi_d1 / (S * sigma * sqrtT) theta = (-(S * phi_d1 * sigma) / (2 * sqrtT) - r * K * math.exp(-r * T) * norm.cdf(d2 if option_type == "call" else -d2)) / 365 vega = S * phi_d1 * sqrtT / 100 result = { "price": round(price, 4), "delta": round(delta, 4), "gamma": round(gamma, 6), "theta": round(theta, 4), "vega": round(vega, 4), "rho": round(rho, 4), } if not include_second_order: return result # Second-order — same for calls and puts (this pricer carries no dividend yield, so the # extra q-term that would otherwise make charm/veta/color differ by option_type is zero). vanna = (-phi_d1 * d2 / sigma) / 100 vomma = (S * phi_d1 * sqrtT * d1 * d2 / sigma) / 10_000 charm = (-phi_d1 * (2 * r * T - d2 * sigma * sqrtT) / (2 * T * sigma * sqrtT)) / 365 veta = (S * phi_d1 * sqrtT * ((r * d1) / (sigma * sqrtT) - (1 + d1 * d2) / (2 * T))) / 36_500 speed = -(gamma / S) * (d1 / (sigma * sqrtT) + 1) color = (phi_d1 / (2 * S * T * sigma * sqrtT) * (2 * r * T + 1 + d1 * (2 * r * T - d2 * sigma * sqrtT) / (sigma * sqrtT))) / 365 zomma = (gamma * (d1 * d2 - 1) / sigma) / 100 result.update({ "vanna": round(vanna, 6), "charm": round(charm, 6), "vomma": round(vomma, 6), "veta": round(veta, 6), "speed": round(speed, 8), "color": round(color, 8), "zomma": round(zomma, 6), }) return result def compute_pnl_curve( S: float, K: float, T: float, r: float, sigma: float, option_type: str, quantity: int, premium_paid: float ) -> List[Dict[str, float]]: """P&L at expiry across a range of underlying prices.""" prices = np.linspace(S * 0.5, S * 1.5, 100) curve = [] for price in prices: if option_type == "call": intrinsic = max(0, price - K) else: intrinsic = max(0, K - price) pnl = (intrinsic - premium_paid) * quantity * 100 curve.append({"underlying": round(float(price), 2), "pnl": round(float(pnl), 2)}) return curve def bull_call_spread(S: float, K_low: float, K_high: float, T: float, r: float, sigma: float) -> Dict[str, Any]: long_call = black_scholes(S, K_low, T, r, sigma, "call") short_call = black_scholes(S, K_high, T, r, sigma, "call") net_debit = long_call["price"] - short_call["price"] max_gain = (K_high - K_low) - net_debit return { "strategy": "Bull Call Spread", "net_debit": round(net_debit, 4), "max_loss": round(net_debit * 100, 2), "max_gain": round(max_gain * 100, 2), "breakeven": round(K_low + net_debit, 2), "legs": [ {"type": "long call", "strike": K_low, "premium": long_call["price"]}, {"type": "short call", "strike": K_high, "premium": short_call["price"]}, ], } def bear_put_spread(S: float, K_high: float, K_low: float, T: float, r: float, sigma: float) -> Dict[str, Any]: long_put = black_scholes(S, K_high, T, r, sigma, "put") short_put = black_scholes(S, K_low, T, r, sigma, "put") net_debit = long_put["price"] - short_put["price"] max_gain = (K_high - K_low) - net_debit return { "strategy": "Bear Put Spread", "net_debit": round(net_debit, 4), "max_loss": round(net_debit * 100, 2), "max_gain": round(max_gain * 100, 2), "breakeven": round(K_high - net_debit, 2), "legs": [ {"type": "long put", "strike": K_high, "premium": long_put["price"]}, {"type": "short put", "strike": K_low, "premium": short_put["price"]}, ], } def long_straddle(S: float, K: float, T: float, r: float, sigma: float) -> Dict[str, Any]: call = black_scholes(S, K, T, r, sigma, "call") put = black_scholes(S, K, T, r, sigma, "put") total_premium = call["price"] + put["price"] return { "strategy": "Long Straddle", "net_debit": round(total_premium, 4), "max_loss": round(total_premium * 100, 2), "max_gain": None, "breakevens": [round(K - total_premium, 2), round(K + total_premium, 2)], "legs": [ {"type": "long call", "strike": K, "premium": call["price"]}, {"type": "long put", "strike": K, "premium": put["price"]}, ], } def implied_vol_surface(S: float, strikes_pct: List[float], expiries_days: List[int], r: float, base_sigma: float) -> List[Dict]: """Generate a simplified IV surface (skew + term structure).""" surface = [] for days in expiries_days: T = days / 365 for pct in strikes_pct: K = S * pct moneyness = math.log(K / S) skew_adj = -0.3 * moneyness # typical negative skew term_adj = 0.02 * math.sqrt(30 / max(days, 1)) iv = max(0.05, base_sigma + skew_adj + term_adj) surface.append({"expiry_days": days, "strike_pct": pct, "strike": round(K, 2), "iv": round(iv, 4)}) return surface