Chapter 1 Laboratory — Module 1: Probability and State Space Explorer¶

Mathematical Foundations of Modern Finance · Part I · Week 1

This notebook drives the same seeded engine as the Module 1 webapp and the Chapter 1 Excel workbook. It implements the complete one-period, two-state market of Section 1.3 and works the four guided experiments (E1–E4). Every number here reproduces the book's running example; the validation checks at the end must all pass.

Book running example (Section 1.3): $S_0=100$, $u=1.2$, $d=0.9$, $R=1.05$, $p=0.6$, call strike $K=105$.

Seeds: 2026CCNN with CC=01 (chapter 1). E1 20260101, E2 20260102, E3 20260103, E4 20260104.

In [1]:
import numpy as np
import matplotlib.pyplot as plt
import mfmf_engine_ch01 as eng

plt.rcParams.update({"figure.figsize": (7, 4.2), "axes.grid": True,
                     "grid.alpha": 0.3, "font.size": 11})
BRASS, INK = "#B4884A", "#0F1E3D"

mkt = eng.MiniMarket()
print("Market:", mkt)
print("No-arbitrage (d < R < u):", mkt.no_arbitrage())
Market: MiniMarket(S0=100.0, u=1.2, d=0.9, R=1.05, p=0.6, B0=1.0)
No-arbitrage (d < R < u): True

The primitives: state prices, risk-neutral probabilities, the SDF¶

Table 1.2 gives one valuation in three notations. The engine computes all three and confirms they agree.

In [2]:
psi_u, psi_d = mkt.state_prices()
q_u, q_d = mkt.risk_neutral()
m_u, m_d = mkt.sdf()

print(f"State prices : psi_u = {psi_u:.4f},  psi_d = {psi_d:.4f}")
print(f"Risk-neutral : q_u   = {q_u:.4f},  q_d   = {q_d:.4f}")
print(f"SDF          : m_u   = {m_u:.4f},  m_d   = {m_d:.4f}")

cu, cd = mkt.call(105.0)
three = mkt.price_three_ways(cu, cd)
print(f"\nCall payoff  : C1(up) = {cu},  C1(down) = {cd}")
print("Call price in three languages:")
for k, v in three.items():
    print(f"   {k:14s}: {v:.6f}")
a, b = mkt.replicating_portfolio(cu, cd)
print(f"\nReplicating portfolio: {a:.4f} shares + {b:.4f} bonds")
State prices : psi_u = 0.4762,  psi_d = 0.4762
Risk-neutral : q_u   = 0.5000,  q_d   = 0.5000
SDF          : m_u   = 0.7937,  m_d   = 1.1905

Call payoff  : C1(up) = 15.0,  C1(down) = 0.0
Call price in three languages:
   state_prices  : 7.142857
   risk_neutral  : 7.142857
   sdf           : 7.142857

Replicating portfolio: 0.5000 shares + -42.8571 bonds

E1 — From a number to a distribution (supports LOS 1.3–1.4)¶

The call's price is a single number, 7.14. Its payoff is a random variable. E1 Monte-Carlos the payoff under the physical measure $P$ and shows that the price is not the discounted physical mean — it is the state-price (risk-neutral) valuation.

In [3]:
r1 = eng.E1_number_to_distribution(seed=20260101)
print(f"State-price value      : {r1['price_state']:.4f}")
print(f"Risk-neutral value     : {r1['price_riskneutral']:.4f}")
print(f"Discounted P-mean       : {r1['discounted_mean_P']:.4f}   <- NOT the price")
print(f"Payoff mean under P     : {r1['payoff_mean_P']:.4f}")
print(f"Payoff std  under P     : {r1['payoff_std_P']:.4f}")

fig, ax = plt.subplots()
ax.bar(["down\n(payoff 0)", "up\n(payoff 15)"], [1-mkt.p, mkt.p],
       color=[INK, BRASS], width=0.6)
ax.axhline(0.5, color="grey", ls="--", lw=1, label="risk-neutral q = 0.5")
ax.set_ylabel("probability"); ax.set_title("E1: payoff is a distribution; price uses q, not p")
ax.legend(); plt.tight_layout(); plt.show()
State-price value      : 7.1429
Risk-neutral value     : 7.1429
Discounted P-mean       : 8.5459   <- NOT the price
Payoff mean under P     : 8.9732
Payoff std  under P     : 7.3539
No description has been provided for this image

E2 — The arbitrage alarm (supports LOS 1.3–1.4)¶

Sweep the gross bond return $R$ across $[0.85, 1.25]$ with prices fixed. As $R \to u=1.2$ the up-state price $\psi_u \to 0$; for $R \ge u$ (or $R \le d$) no state-price vector exists and the alarm fires with an explicit arbitrage portfolio.

In [4]:
r2 = eng.E2_arbitrage_sweep(seed=20260102)
R = np.array(r2["R"]); psi = np.array(r2["psi_u"], dtype=float)

fig, ax = plt.subplots()
ax.plot(R, psi, color=INK, lw=2)
ax.axvspan(0.85, r2["d"], color="crimson", alpha=0.12, label="alarm: R ≤ d")
ax.axvspan(r2["u"], 1.25, color="crimson", alpha=0.12, label="alarm: R ≥ u")
ax.axvline(1.05, color=BRASS, ls="--", lw=1.5, label="book R = 1.05")
ax.set_xlabel("gross bond return R"); ax.set_ylabel("state price psi_u")
ax.set_title("E2: psi_u collapses to 0 as R -> u; alarm outside (d, u)")
ax.legend(); plt.tight_layout(); plt.show()

# explicit arbitrage witness at R = 1.25
arb = eng.MiniMarket(R=1.25).arbitrage_portfolio()
print("Arbitrage portfolio at R = 1.25:", arb)
No description has been provided for this image
Arbitrage portfolio at R = 1.25: {'shares': -1.0, 'bonds': 100.0, 'V0': 0.0, 'Vu': 5.0, 'Vd': 35.0}

E3 — P versus Q: prices don't move, the optimal allocation does (supports LOS 1.4)¶

Vary the physical probability $p$ across $(0,1)$ with prices fixed. No pricing output moves — the price used $q$, not $p$. But the log-utility investor's optimal stock allocation $\alpha^\*$ swings from short to leveraged, crossing zero exactly at $p=q=0.5$. This is the single most reliable source of P-vs-Q confusion, resolved.

In [5]:
r3 = eng.E3_P_vs_Q(seed=20260103)
p = np.array(r3["p"]); alpha = np.array(r3["alpha_star"]); q = r3["q"]

fig, ax = plt.subplots()
ax.plot(p, alpha, color=INK, lw=2)
ax.axhline(0, color="grey", lw=1)
ax.axvline(q, color=BRASS, ls="--", lw=1.5, label=f"p = q = {q:.2f}")
ax.scatter([q], [0], color=BRASS, zorder=5)
ax.set_xlabel("physical probability p"); ax.set_ylabel("optimal stock allocation alpha*")
ax.set_title("E3: alpha* crosses zero exactly at p = q; the price never moved")
ax.legend(); plt.tight_layout(); plt.show()
print(f"Price of the call, fixed across all p: {r3['price_fixed']:.4f}")
No description has been provided for this image
Price of the call, fixed across all p: 7.1429

E4 — Add a third state, keep two assets: the price becomes a bound (supports LOS 1.3)¶

Introduce a third state with no third asset. The market is now incomplete: a claim that pays 1 only in the middle state is not replicable, and its unique price dissolves into a no-arbitrage interval. This is the door into Chapter 4.

In [6]:
r4 = eng.E4_third_state_bound(seed=20260104)
print(f"No-arbitrage price interval for the digital claim: "
      f"[{r4['lower']:.4f}, {r4['upper']:.4f}]")
print(f"Interval width: {r4['upper'] - r4['lower']:.4f}")
print(r4["note"])

fig, ax = plt.subplots(figsize=(7, 1.8))
ax.hlines(0, r4["lower"], r4["upper"], color=BRASS, lw=8)
ax.plot([r4["lower"], r4["upper"]], [0, 0], "|", color=INK, ms=18, mew=2)
ax.set_xlim(-0.05, 0.8); ax.set_yticks([])
ax.set_title("E4: the unique price is gone — only a bound remains")
ax.set_xlabel("price of the non-replicable claim"); plt.tight_layout(); plt.show()
No-arbitrage price interval for the digital claim: [0.0000, 0.7143]
Interval width: 0.7143
unique price -> interval once the market is incomplete
No description has been provided for this image

Validation checks¶

Every laboratory report must reproduce these. A report whose checks do not pass is returned ungraded.

In [7]:
checks = eng.validation_checks()
for k, v in checks.items():
    if k.startswith("_"):
        continue
    mark = "PASS" if v else "FAIL"
    print(f"[{mark}] {k}")
assert checks["ALL_PASS"], "Validation failed — do not submit."
print("\nAll Module 1 validation checks pass.")
[PASS] V1_no_arbitrage
[PASS] V2_psi_u
[PASS] V3_q_half
[PASS] V4_three_languages_agree
[PASS] V5_call_price
[PASS] V6_bond_prices_to_1
[PASS] ALL_PASS

All Module 1 validation checks pass.