Payoff diagrams for multi-leg strategies
A payoff diagram shows P&L at expiry across a range of underlying prices — the standard way to visualize and verify a multi-leg options strategy (chapter 60) before putting it on.
import numpy as np
import matplotlib.pyplot as plt
def leg_payoff_at_expiry(spot_range: np.ndarray, strike: float, premium: float, option_type: str, position: str) -> np.ndarray:
"""position: 'long' or 'short'"""
if option_type == "CE":
intrinsic = np.maximum(spot_range - strike, 0)
else:
intrinsic = np.maximum(strike - spot_range, 0)
payoff = intrinsic - premium if position == "long" else premium - intrinsic
return payoff
def combined_payoff(spot_range: np.ndarray, legs: list[dict]) -> np.ndarray:
"""legs: [{strike, premium, option_type, position, qty}, ...]"""
total = np.zeros_like(spot_range, dtype=float)
for leg in legs:
total += leg_payoff_at_expiry(spot_range, leg["strike"], leg["premium"], leg["option_type"], leg["position"]) * leg["qty"]
return total
Worked example: bull call spread (chapter 60)
spot_range = np.arange(23500, 25500, 25)
bull_call_spread = [
{"strike": 24500, "premium": 180, "option_type": "CE", "position": "long", "qty": 1},
{"strike": 25000, "premium": 60, "option_type": "CE", "position": "short", "qty": 1},
]
payoff = combined_payoff(spot_range, bull_call_spread)
plt.plot(spot_range, payoff)
plt.axhline(0, color="black", linewidth=0.5)
plt.xlabel("NIFTY at expiry")
plt.ylabel("P&L")
plt.title("Bull Call Spread Payoff")
plt.show()
Extracting key metrics directly from the payoff array
def payoff_metrics(spot_range: np.ndarray, payoff: np.ndarray) -> dict:
max_profit = payoff.max()
max_loss = payoff.min()
breakeven_indices = np.where(np.diff(np.sign(payoff)))[0]
breakevens = spot_range[breakeven_indices].tolist()
return {"max_profit": max_profit, "max_loss": max_loss, "breakevens": breakevens}
Worked example: short straddle (higher-risk, common in Indian index expiry trading)
short_straddle = [
{"strike": 24500, "premium": 180, "option_type": "CE", "position": "short", "qty": 1},
{"strike": 24500, "premium": 165, "option_type": "PE", "position": "short", "qty": 1},
]
payoff = combined_payoff(spot_range, short_straddle)
metrics = payoff_metrics(spot_range, payoff)
print(metrics) # max_profit is capped (total premium collected), max_loss is theoretically unbounded
This immediately shows the asymmetric risk profile that a short straddle carries — capped profit, open-ended loss — visually and numerically, before any capital is committed. This is exactly the kind of check chapter 85's overfitting/scrutiny checklist implicitly assumes you'd do for any strategy someone pitches you: does the payoff structure actually match the risk you think you're taking?
Adding current-Greeks context to the static payoff (a fuller pre-trade check)
def strategy_greeks_at_entry(legs: list[dict], S: float, T: float, r: float, iv_per_leg: list[float]) -> dict:
positions = [
{"quantity": (1 if leg["position"] == "long" else -1) * leg["qty"], "lot_size": 1,
**compute_greeks(S, leg["strike"], T, r, iv, leg["option_type"])}
for leg, iv in zip(legs, iv_per_leg)
]
return aggregate_portfolio_greeks(positions) # chapter 122
The payoff diagram shows the *expiry* outcome; the Greeks show the *right now* sensitivity — a strategy can look fine at expiry on paper while carrying uncomfortable interim Vega/Gamma exposure well before expiry arrives (chapter 122's point about needing both views, not just P&L).