Chapter 9 Laboratory — Module 9: The Allocation Laboratory¶

Mathematical Foundations of Modern Finance · Part III · Week 9

Four panels: the frontier, the estimation-fragility panel, the Merton dynamic solution, and the Kelly growth panel. Reproduces the tangency weights 16.5 / 37.0 / 46.5 and the Merton 60% weight at γ = 1.74.

Seeds: 20260901–20260904.

In [1]:
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams.update({"figure.figsize": (7, 4.2), "axes.grid": True,
                     "grid.alpha": 0.3, "font.size": 11})
BRASS, INK = "#B4884A", "#0F1E3D"
import mfmf_engine_ch09 as eng

E1 — The tangency portfolio and the Sharpe gap (supports LOS 9.2)¶

From the capital-market assumptions, the tangency weights are 16.5 / 37.0 / 46.5. A 60/40-style policy leaves a measurable Sharpe gap to efficiency.

In [2]:
r1 = eng.E1_tangency()
print(f"Tangency weights (%) : {r1['tangency_weights_pct']}   (book: 16.5 / 37.0 / 46.5)")
print(f"Tangency Sharpe      : {r1['tangency_sharpe']:.4f}")
print(f"Policy Sharpe        : {r1['policy_sharpe']:.4f}")
print(f"Sharpe gap           : {r1['sharpe_gap']:.4f}")

w = r1['tangency_weights_pct']
fig, ax = plt.subplots()
ax.bar(["Asset 1", "Asset 2", "Asset 3"], w, color=[INK, BRASS, "#5A6B82"])
ax.set_ylabel("tangency weight (%)"); ax.set_title("E1: the tangency portfolio")
plt.tight_layout(); plt.show()
Tangency weights (%) : [16.5, 37.0, 46.5]   (book: 16.5 / 37.0 / 46.5)
Tangency Sharpe      : 0.5468
Policy Sharpe        : 0.5426
Sharpe gap           : 0.0042
No description has been provided for this image

E2 — Estimation fragility and its defenses (supports LOS 9.3)¶

Tangency weights are exquisitely sensitive to the mean inputs. Resampling the means produces a wide cloud of weights; shrinkage toward the cross-sectional average tames it.

In [3]:
r2 = eng.E2_fragility()
print(f"Base weights (%)        : {r2['base_weights_pct']}")
print(f"Raw weight dispersion   : {r2['raw_weight_dispersion']:.4f}")
print(f"Shrunk weight dispersion: {r2['shrunk_weight_dispersion']:.4f}")
print(f"Shrinkage tames the cloud: {r2['shrinkage_tames']}")
Base weights (%)        : [16.5, 37.0, 46.5]
Raw weight dispersion   : 0.2047
Shrunk weight dispersion: 0.1589
Shrinkage tames the cloud: True

E3 — Merton's dynamic solution (supports LOS 9.4–9.5)¶

The dynamic problem has a constant-weight solution $w^\* = (\mu-r_f)/(\gamma\sigma^2)$. At $\gamma = 1.74$ this is the 60% risky weight — and the investment horizon is irrelevant.

In [4]:
r3 = eng.E3_merton()
print(f"Merton weight at gamma=1.74 : {r3['merton_weight']:.4f}   (book: 60%)")
print(f"Implied gamma for a 60% weight: {r3['implied_gamma_for_60pct']:.4f}")
print(f"Horizon irrelevant          : {r3['horizon_irrelevant']}")
Merton weight at gamma=1.74 : 0.6005   (book: 60%)
Implied gamma for a 60% weight: 1.7415
Horizon irrelevant          : True

E4 — The Kelly criterion and the growth ceiling (supports LOS 9.6)¶

The log-growth rate is a parabola in the risky weight. Kelly sits at the vertex; twice-Kelly gives back all the growth (zero), the growth ceiling made concrete.

In [5]:
r4 = eng.E4_kelly()
print(f"Kelly fraction     : {r4['kelly_fraction']:.4f}")
print(f"g at 60%           : {r4['g_at_60pct']:.5f}")
print(f"g at Kelly         : {r4['g_at_kelly']:.5f}")
print(f"g at twice-Kelly   : {r4['g_at_twice_kelly']:.5f}   (zero growth)")

ws = np.linspace(0, 2*r4['kelly_fraction'], 100)
excess, sigma = 0.052-0.02, 0.175
g = ws*excess - 0.5*ws**2*sigma**2
fig, ax = plt.subplots()
ax.plot(ws, g, color=INK, lw=2)
ax.axvline(r4['kelly_fraction'], color=BRASS, ls="--", lw=1.5, label="Kelly")
ax.axhline(0, color="grey", lw=1)
ax.set_xlabel("risky weight w"); ax.set_ylabel("log-growth rate g(w)")
ax.set_title("E4: the growth parabola; Kelly at the vertex")
ax.legend(); plt.tight_layout(); plt.show()
Kelly fraction     : 1.0449
g at 60%           : 0.01369
g at Kelly         : 0.01672
g at twice-Kelly   : 0.00000   (zero growth)
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 [6]:
checks = eng.validation_checks()
for k, v in checks.items():
    if k.startswith("_"): continue
    print(f"[{'PASS' if v else 'FAIL'}] {k}")
assert checks["ALL_PASS"], "Validation failed — do not submit."
print("\nAll Module validation checks pass.")
[PASS] V1_tangency_16.5_37_46.5
[PASS] V2_sharpe_gap_positive
[PASS] V3_merton_60pct_at_gamma_1.74
[PASS] V4_twice_kelly_zero_growth
[PASS] ALL_PASS

All Module validation checks pass.