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.
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.
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.
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
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.
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)
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.
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}")
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.
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
Validation checks¶
Every laboratory report must reproduce these. A report whose checks do not pass is returned ungraded.
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.