codex/put-call-parity-replication.py

put-call-parity-replication.py
"""Draw the identical-cash-flow replication behind put-call parity.

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

from math import exp, isclose
from pathlib import Path

import matplotlib

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


def main():
    stock, strike, maturity, rate, call_price = 100, 100, 1, 0.05, 10
    bond_price = strike * exp(-rate * maturity)
    put_price = call_price + bond_price - stock
    cost_a = call_price + bond_price
    cost_b = put_price + stock
    assert isclose(cost_a, cost_b, abs_tol=1e-12)
    prices = np.linspace(60, 140, 401)
    call = np.maximum(prices - strike, 0)
    put = np.maximum(strike - prices, 0)
    np.testing.assert_allclose(call + strike, put + prices, atol=1e-12)

    plt.rcParams.update({"font.size": 11, "axes.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(9, 4.7), layout="constrained",
                             facecolor="white")
    fig.suptitle("Put-call parity: two ways to deliver the same cash flow", fontsize=15)
    blue, orange, green = "#2563a6", "#b76816", "#347456"
    ax = axes[0]
    ax.set(xlim=(0, 1), ylim=(0, 1))
    ax.axis("off")
    ax.set_title("Today: S₀ = K = 100, T = 1, r = 0.05", fontsize=11, pad=12)
    ax.text(0.5, 0.95, "Illustrative call price C₀ = 10", ha="center", va="top")
    ax.text(0.5, 0.75,
            f"A: call + bond paying 100 at expiry\n"
            f"10 + 100 exp(−0.05)\n= {cost_a:.8f}",
            ha="center", va="center", linespacing=1.6, color=blue,
            bbox={"boxstyle": "round,pad=0.7", "facecolor": "#f1f6fc",
                  "edgecolor": blue})
    ax.text(0.5, 0.43,
            f"B: put + one stock share\n{put_price:.8f} + 100\n= {cost_b:.8f}",
            ha="center", va="center", linespacing=1.6, color=orange,
            bbox={"boxstyle": "round,pad=0.7", "facecolor": "#fdf6ed",
                  "edgecolor": orange})
    ax.text(0.5, 0.12, "Identical terminal cash flows\n⇒ equal initial costs without arbitrage",
            ha="center", va="center", linespacing=1.5)

    ax = axes[1]
    ax.plot(prices, call + strike, color=blue, linewidth=4,
            label=r"A: call + bond, $C_T+100$")
    ax.plot(prices, put + prices, color=orange, linewidth=2, linestyle="--",
            label=r"B: put + stock, $P_T+S_T$")
    scenarios = [80, 100, 120]
    values = [100, 100, 120]
    ax.scatter(scenarios, values, s=40, color=green, zorder=4)
    for price, value, offset in [(80, 100, (-28, -26)),
                                  (100, 100, (5, -26)),
                                  (120, 120, (5, 8))]:
        ax.annotate(f"({price}, {value})", (price, value), xytext=offset,
                    textcoords="offset points", fontsize=10)
    ax.axvline(strike, color="#777777", linestyle=":", linewidth=1)
    ax.set(xlim=(60, 140), ylim=(85, 145), xticks=[60, 80, 100, 120, 140],
           xlabel=r"Terminal stock price $S_T$", ylabel="Terminal portfolio amount",
           title=r"At expiry: $A_T=B_T=\max(S_T,100)$",)
    ax.legend(loc="upper left", fontsize=10)
    ax.grid(alpha=0.18)
    fig.savefig(Path.cwd() / "put-call-parity-replication.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()