1"""Phase 3 — calibration of the classical baseline: sweep a few behavioural parameters over seeds and2report the stylized facts (tracking error, return autocorrelation, kurtosis, volatility clustering,3spread/depth, bubbles). Writes results/calibration/sweep.parquet and a summary CSV. No API calls."""4from __future__ import annotations56import itertools7import sys8from concurrent.futures import ProcessPoolExecutor9from pathlib import Path1011import pandas as pd1213sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))14from market_agents.experiments.config import RunConfig # noqa: E40215from market_agents.experiments.runner import run_config # noqa: E40216from market_agents.metrics.outcomes import run_outcomes # noqa: E4021718ROOT = Path(__file__).resolve().parents[1]19GRID = {20 "fund_aggr": [10.0, 20.0, 40.0], # fundamentalist lots per 1 % mispricing21 "trend_share": [0.10, 0.15, 0.25], # trend+leveraged share (rebalanced against noise)22 "noise_pm": [0.3, 0.6], # noise traders' market-order probability23 "mm_qty": [10, 20], # market-maker quote size24}25SEEDS = list(range(1, 11))262728def make_cfg(fund_aggr, trend_share, noise_pm, mm_qty, seed):29 tr = trend_share * 0.630 lev = trend_share * 0.431 noise = 1.0 - (0.25 + 0.10 + 0.05 + tr + lev)32 mix = {"fundamentalist": 0.25, "trend": round(tr, 4), "meanrev": 0.10, "noise": round(noise, 4), "market_maker": 0.05,33 "statarb": 0.0, "leveraged": round(lev, 4)}34 mix["noise"] = round(1.0 - sum(v for k, v in mix.items() if k != "noise"), 6)35 return RunConfig.from_yaml(ROOT / "configs/baseline.yaml", **{36 "name": f"cal_fa{fund_aggr}_ts{trend_share}_pm{noise_pm}_mm{mm_qty}", "seed": seed, "n_ticks": 600,37 "population.mix": mix,38 "population.policy_kwargs": {"fundamentalist": {"aggressiveness": fund_aggr}, "noise": {"p_market": noise_pm},39 "market_maker": {"quote_qty": mm_qty}},40 "treatment": {"fund_aggr": fund_aggr, "trend_share": trend_share, "noise_pm": noise_pm, "mm_qty": mm_qty}})414243def one(args):44 cfg = make_cfg(*args)45 return run_outcomes(run_config(cfg))464748if __name__ == "__main__":49 jobs = [(*g, s) for g in itertools.product(*GRID.values()) for s in SEEDS]50 print(f"{len(jobs)} runs")51 with ProcessPoolExecutor(max_workers=8) as ex:52 rows = list(ex.map(one, jobs, chunksize=4))53 df = pd.DataFrame(rows)54 out = ROOT / "results/calibration"55 out.mkdir(parents=True, exist_ok=True)56 df.to_parquet(out / "sweep.parquet", index=False)57 keys = ["tr_fund_aggr", "tr_trend_share", "tr_noise_pm", "tr_mm_qty"]58 cols = ["mad_pct", "acf_r_1", "vr5", "kurtosis", "acf_absr_1_10", "vol_bp", "spread_bp", "depth5", "lambda_bp_100",59 "n_bubbles", "max_dd", "time_to_absorb", "response_ratio", "volume", "pnl_market_maker"]60 summ = df.groupby(keys)[cols].mean().round(3)61 summ.to_csv(out / "sweep_summary.csv")62 print(summ.to_string())63