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).

Next: 125 — Delta-neutral hedging