1"""Model C factorial experiments with the Bayesian mean-variance control (no LLM): market design × delegation share ×2model diversity × leverage, 20 seeds (common random numbers across cells), plus stress shocks.34Outputs: results/model_c/factorial.csv (one row per cell × seed), results/model_c/stress.csv,5results/model_c/summary.csv (means and bootstrap 95% CIs by cell).6Run: python3 scripts/30_model_c.py [--quick]7"""8from __future__ import annotations910import itertools11import sys12from pathlib import Path1314import numpy as np15import pandas as pd1617sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src'))18from age.markets.simulation import MarketConfig, run_market # noqa: E4021920ROOT = Path(__file__).resolve().parents[1]21OUT = ROOT / 'results' / 'model_c'22OUT.mkdir(parents=True, exist_ok=True)23QUICK = '--quick' in sys.argv24SEEDS = range(1, 6) if QUICK else range(1, 21)25T = 80 if QUICK else 16026KEYS = ['vol', 'of_corr', 'of_corr_deleg', 'of_corr_human', 'pressure', 'r2_common', 'kyle_lambda', 'spread', 'depth',27 'dispersion', 'defaults', 'forced', 'losses', 'leverage', 'n_trades', 'mm_inventory']282930def cell(design, D, M, lev, seed, **kw):31 out = run_market(MarketConfig(design=design, delegation_share=D, M=M, max_leverage=lev, seed=seed, T=T, **kw))32 row = dict(design=design, D=D, M=M, leverage_cap=lev, seed=seed)33 row.update({k: out.get(k, np.nan) for k in KEYS})34 for k in ('shock_return', 'shock_forced', 'shock_defaults', 'shock_min_return', 'shock_trough', 'shock_losses'):35 row[k] = out.get(k, np.nan)36 return row373839STRESS_ONLY = '--stress-only' in sys.argv40rows = []41for design, D, M, lev in ([] if STRESS_ONLY else itertools.product(['walrasian', 'cda', 'bilateral'], [0.0, 0.25, 0.5, 0.75, 1.0], [1, 2, 4, 8], [0.0, 0.5])):42 if D == 0.0 and M > 1:43 continue44 for s in SEEDS:45 rows.append(cell(design, D, M, lev, s))46df = pd.DataFrame(rows) if rows else pd.read_csv(OUT / 'factorial.csv')47df.to_csv(OUT / 'factorial.csv', index=False)4849# stress: a 3-sd common forecast shock at period T/2, with and without leverage, by design and M50srows = []51STRESS = dict(sig_A=0.02, rho_d=0.95, liquidation_impact=1.0, mm_tick=0.0025, mm_levels=40, mm_depth=0.1,52 mm_inventory_max=10, auto_repay=False)53for design, M, persist, margin, pre in itertools.product(['walrasian', 'cda'], [1, 4], [1, 3], [0.5, 0.2], [0.0, 0.95]):54 for s in SEEDS:55 r = cell(design, 1.0, M, 0.0, s, shock_period=T // 2, shock_size=-10.0, shock_persist=persist, margin=margin,56 preload_leverage=pre, **STRESS)57 r.update(margin=margin, persist=persist, preload=pre, control=False)58 srows.append(r)59 for s in SEEDS: # control: same seeds, no shock60 r = cell(design, 1.0, M, 0.0, s, margin=margin, preload_leverage=pre, **STRESS)61 r.update(margin=margin, persist=persist, preload=pre, control=True, shock_return=np.nan)62 srows.append(r)63sdf = pd.DataFrame(srows)64sdf['control'] = sdf.get('control', False).fillna(False)65sdf.to_csv(OUT / 'stress.csv', index=False)666768def boot_ci(x, n=1000, seed=0):69 x = np.asarray(x, float)70 x = x[~np.isnan(x)]71 if len(x) == 0:72 return np.nan, np.nan, np.nan73 rng = np.random.default_rng(seed)74 means = [rng.choice(x, len(x)).mean() for _ in range(n)]75 return x.mean(), np.percentile(means, 2.5), np.percentile(means, 97.5)767778summ = []79for keys, g in df.groupby(['design', 'D', 'M', 'leverage_cap']):80 row = dict(zip(['design', 'D', 'M', 'leverage_cap'], keys))81 for k in KEYS:82 mu, lo, hi = boot_ci(g[k])83 row[k], row[k + '_lo'], row[k + '_hi'] = mu, lo, hi84 summ.append(row)85pd.DataFrame(summ).to_csv(OUT / 'summary.csv', index=False)86ssumm = []87for keys, g in sdf.groupby(['design', 'M', 'persist', 'margin', 'preload', 'control']):88 row = dict(zip(['design', 'M', 'persist', 'margin', 'preload', 'control'], keys))89 for k in ['shock_return', 'shock_min_return', 'shock_trough', 'shock_forced', 'shock_defaults', 'shock_losses', 'vol', 'defaults', 'forced', 'losses', 'kyle_lambda', 'mm_inventory', 'leverage']:90 mu, lo, hi = boot_ci(g[k])91 row[k], row[k + '_lo'], row[k + '_hi'] = mu, lo, hi92 ssumm.append(row)93pd.DataFrame(ssumm).to_csv(OUT / 'stress_summary.csv', index=False)94print(pd.DataFrame(summ)[['design', 'D', 'M', 'leverage_cap', 'vol', 'of_corr', 'of_corr_deleg', 'r2_common', 'defaults']].to_string())95print(pd.DataFrame(ssumm)[['design', 'M', 'persist', 'margin', 'preload', 'control', 'shock_return', 'shock_trough', 'shock_forced', 'shock_defaults', 'leverage']].to_string())96