codex/european-option-payoffs.py

european-option-payoffs.py
"""Draw deterministic European option payoffs, with no pricing model.

Tested with Python 3.14.4, numpy 2.3.5, matplotlib 3.10.7.
Run from _media: python ../european-option-payoffs.py
The PNG is written into the current working directory.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    strike = 100
    prices = np.linspace(60, 140, 401)
    call = np.maximum(prices - strike, 0)
    put = np.maximum(strike - prices, 0)
    np.testing.assert_allclose(call - put, prices - strike, atol=1e-12)

    plt.rcParams.update({"font.size": 11, "axes.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(9, 4.4), layout="constrained",
                             facecolor="white")
    fig.suptitle("European options at expiry: strike K = 100", fontsize=15)
    ax = axes[0]
    ax.plot(prices, call, color="#2563a6", linewidth=2.5,
            label=r"Call: $(S_T-K)^+$")
    ax.plot(prices, put, color="#b76816", linewidth=2.5,
            label=r"Put: $(K-S_T)^+$")
    scenarios = np.array([80, 100, 120])
    ax.scatter(scenarios, np.maximum(scenarios - strike, 0),
               color="#2563a6", s=38, zorder=4)
    ax.scatter(scenarios, np.maximum(strike - scenarios, 0),
               color="#b76816", s=38, marker="s", zorder=3)
    ax.set(ylim=(-3, 43), ylabel="Terminal payoff per share",
           title="Each exercise right pays only its positive part")
    ax.legend(loc="upper center", fontsize=10)

    ax = axes[1]
    ax.axhline(0, color="#777777", linewidth=1)
    ax.plot(prices, call - put, color="#347456", linewidth=2.5,
            label=r"Difference: $C_T-P_T=S_T-K$")
    ax.scatter(scenarios, scenarios - strike, color="#347456", s=38, zorder=4)
    for price, offset in [(80, (10, -18)), (100, (10, -18)), (120, (-70, 12))]:
        ax.annotate(f"({price}, {price - strike})", (price, price - strike),
                    xytext=offset, textcoords="offset points", fontsize=10)
    ax.set(ylim=(-43, 43), ylabel="Difference of terminal payoffs",
           title="Subtracting cancels the corner")
    ax.legend(loc="upper left", fontsize=10)

    for ax in axes:
        ax.axvline(strike, color="#777777", linestyle="--", linewidth=1)
        ax.set(xlim=(60, 140), xlabel=r"Terminal stock price $S_T$",
               xticks=[60, 80, 100, 120, 140])
        ax.grid(alpha=0.18)
    fig.savefig(Path.cwd() / "european-option-payoffs.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()