import sys, numpy as np, matplotlib.pyplot as plt
sys.path.insert(0,'..')
from engine import ch08
from dataclasses import replaceMFAA Chapter 8 Laboratory
Fund Cash-Flow Engine (book §8.9)
Generate portfolio paths, push them through calls, fees, and a tier-tracked waterfall, and report the LP’s world. The engine on which Chapters 9–10 mount asset-specific dynamics. Seed 20260800.
1. The J-curve fan
sim = ch08.simulate_fund(ch08.FundParams(M=8000))
net_cum = np.cumsum(sim['call_flow']+sim['dist_flow']+sim['fee_flow'], axis=0)
t = sim['tgrid']
plt.plot(t, net_cum.mean(axis=1), 'C0-', label='mean')
plt.fill_between(t, np.quantile(net_cum,.05,axis=1), np.quantile(net_cum,.95,axis=1), alpha=.2, label='5–95%')
plt.axhline(0, color='grey', lw=.6); plt.legend(); plt.title('J-curve fan (per 100 committed)'); plt.xlabel('years');
2. Ex 8.10 — tier-tracked European waterfall
Conservation W_LP + W_GP = G to the cent, on every path.
import numpy as np
called=np.array([100.,100.,100.,100.]); G=np.array([80.,108.,120.,200.])
wf = ch08.european_waterfall(G, called, ch08.FundParams(pref_rate=0.08), years=1.0)
for i in range(4): print(f'G={G[i]:.0f}: LP {wf["lp"][i]:.2f}, GP {wf["gp"][i]:.2f}, sum {wf["lp"][i]+wf["gp"][i]:.2f}')
print('max conservation error:', wf['conservation_error'])G=80: LP 80.00, GP 0.00, sum 80.00
G=108: LP 108.00, GP 0.00, sum 108.00
G=120: LP 116.00, GP 4.00, sum 120.00
G=200: LP 180.00, GP 20.00, sum 200.00
max conservation error: 0.0
3. E3 — Manufacture an IRR with a subscription line
The subscription line shifts IRR right while PME stands still (Proposition 8.6(ii)).
import pandas as pd
pd.DataFrame(ch08.subscription_line_demo(ch08.FundParams(M=4000)))| delay_quarters | pme_mean | tvpi_mean | irr_proxy | |
|---|---|---|---|---|
| 0 | 0 | 1.241557 | 1.970195 | 0.000 |
| 1 | 1 | 1.241557 | 1.970195 | 0.015 |
| 2 | 2 | 1.241557 | 1.970195 | 0.030 |
| 3 | 3 | 1.241557 | 1.970195 | 0.045 |
| 4 | 4 | 1.241557 | 1.970195 | 0.060 |
4. Performance measures & validation
pm = ch08.performance_measures(ch08.FundParams(M=8000))
print(f"TVPI {pm['tvpi_mean']:.3f}, DPI {pm['dpi_mean']:.3f}, PME {pm['pme_mean']:.3f}")
v = ch08.validation_checks()
for k,d in v.items():
if isinstance(d,dict): print(k, 'PASS' if d['pass_'] else 'FAIL')
print('ALL:', v['all_pass'])TVPI 1.960, DPI 1.960, PME 1.235
V1_conservation PASS
V2_waterfall_map PASS
V3_deterministic_limit PASS
V4_irr_roots PASS
V5_pme_stable PASS
V6_reproducible PASS
ALL: True