sim_portfolio_v2.py
"""Extended analysis with downside risk and confidence-level views."""
import json
import numpy as np
HIST_B = np.array([
17.87, 10.86, 9.96, 11.96, 1.83, 11.16,
14.95, 13.96, 6.96, 25.50, 13.85, 19.58
]) / 100.0
R_A = 0.065
HORIZON = 3
TARGET = 1.40
N_SIM = 200_000
rng = np.random.default_rng(seed=42)
samples = rng.choice(HIST_B, size=(N_SIM, HORIZON), replace=True)
mult_A_3yr = (1 + R_A) ** HORIZON
mult_B_paths = np.prod(1 + samples, axis=1)
print(f"Asset A 3-yr multiple: {mult_A_3yr:.4f}")
print(f"Asset B 3-yr P(>=1.4x): {(mult_B_paths >= TARGET).mean()*100:.2f}%")
print(f"Asset B 3-yr P(<1.0x): {(mult_B_paths < 1.0).mean()*100:.4f}% (only counts price risk, not 1% principal)")
print()
# Fine sweep
ws_fine = np.linspace(0, 1, 101)
fine = []
for w in ws_fine:
final = (1 - w) * mult_A_3yr + w * mult_B_paths
fine.append({
"w": float(w),
"w_pct": round(float(w) * 100, 1),
"p_target": float((final >= TARGET).mean()),
"p_loss": float((final < 1.0).mean()),
"mean": float(final.mean()),
"median": float(np.median(final)),
"p1": float(np.percentile(final, 1)),
"p5": float(np.percentile(final, 5)),
"p25": float(np.percentile(final, 25)),
"p75": float(np.percentile(final, 75)),
"p95": float(np.percentile(final, 95)),
"p99": float(np.percentile(final, 99)),
})
# Determine, for each confidence threshold, the minimum w to reach it
conf_levels = [0.30, 0.40, 0.50, 0.60, 0.6493]
min_w_for_conf = {}
for c in conf_levels:
hits = [r for r in fine if r["p_target"] >= c]
if hits:
min_w = min(hits, key=lambda r: r["w"])
min_w_for_conf[c] = min_w
else:
min_w_for_conf[c] = None
print("Minimum w_B for confidence levels:")
for c, r in min_w_for_conf.items():
if r:
print(f" >={c*100:.1f}% confidence: w_B>={r['w']*100:.1f}% (P5={r['p5']:.4f}, mean={r['mean']:.4f})")
else:
print(f" >={c*100:.1f}% confidence: NOT ACHIEVABLE")
# A coarse w table for display
coarse = []
for w_pct in [0, 25, 50, 60, 70, 75, 80, 90, 100]:
w = w_pct / 100
final = (1 - w) * mult_A_3yr + w * mult_B_paths
coarse.append({
"w_pct": w_pct,
"p_target": float((final >= TARGET).mean()),
"p_loss": float((final < 1.0).mean()),
"mean": float(final.mean()),
"median": float(np.median(final)),
"p5": float(np.percentile(final, 5)),
"p95": float(np.percentile(final, 95)),
"p25": float(np.percentile(final, 25)),
"p75": float(np.percentile(final, 75)),
"implied_cagr_mean_pct": (float(final.mean()) ** (1/3) - 1) * 100,
"implied_cagr_p5_pct": (float(np.percentile(final, 5)) ** (1/3) - 1) * 100,
"implied_cagr_p95_pct": (float(np.percentile(final, 95)) ** (1/3) - 1) * 100,
})
print()
print(f"{'w_B%':>5} {'P(>=1.4x)':>10} {'P(<1x)':>8} {'mean':>7} {'median':>7} {'P5':>7} {'P95':>7} {'CAGR mean':>10} {'CAGR P5':>9} {'CAGR P95':>9}")
for r in coarse:
print(f"{r['w_pct']:>5} {r['p_target']*100:>9.2f}% {r['p_loss']*100:>7.3f}% "
f"{r['mean']:>7.4f} {r['median']:>7.4f} {r['p5']:>7.4f} {r['p95']:>7.4f} "
f"{r['implied_cagr_mean_pct']:>9.2f}% {r['implied_cagr_p5_pct']:>8.2f}% {r['implied_cagr_p95_pct']:>8.2f}%")
# Save extended JSON
out = {
"hist_b_pct": [round(x*100, 2) for x in HIST_B.tolist()],
"years": list(range(2014, 2026)),
"hist_stats": {
"n": int(len(HIST_B)),
"mean_pct": float(HIST_B.mean()*100),
"std_pct": float(HIST_B.std(ddof=1)*100),
"min_pct": float(HIST_B.min()*100),
"max_pct": float(HIST_B.max()*100),
"median_pct": float(np.median(HIST_B)*100),
},
"asset_a_annual_pct": R_A * 100,
"asset_a_3yr_mult": float(mult_A_3yr),
"horizon_yrs": HORIZON,
"target_mult": TARGET,
"n_sim": N_SIM,
"fine": fine,
"coarse": coarse,
"asset_b_3yr": {
"mean": float(mult_B_paths.mean()),
"median": float(np.median(mult_B_paths)),
"p5": float(np.percentile(mult_B_paths, 5)),
"p95": float(np.percentile(mult_B_paths, 95)),
"p_target": float((mult_B_paths >= TARGET).mean()),
"p_loss": float((mult_B_paths < 1.0).mean()),
},
}
with open("sim_results.json", "w") as f:
json.dump(out, f, indent=2)
print("\nWrote sim_results.json")
The 2 main “buttons” identified so far in this thread - FDs and Mutual-funds…