"""Phase 3 — calibration of the classical baseline: sweep a few behavioural parameters over seeds and report the stylized facts (tracking error, return autocorrelation, kurtosis, volatility clustering, spread/depth, bubbles). Writes results/calibration/sweep.parquet and a summary CSV. No API calls.""" from __future__ import annotations import itertools import sys from concurrent.futures import ProcessPoolExecutor from pathlib import Path import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from market_agents.experiments.config import RunConfig # noqa: E402 from market_agents.experiments.runner import run_config # noqa: E402 from market_agents.metrics.outcomes import run_outcomes # noqa: E402 ROOT = Path(__file__).resolve().parents[1] GRID = { "fund_aggr": [10.0, 20.0, 40.0], # fundamentalist lots per 1 % mispricing "trend_share": [0.10, 0.15, 0.25], # trend+leveraged share (rebalanced against noise) "noise_pm": [0.3, 0.6], # noise traders' market-order probability "mm_qty": [10, 20], # market-maker quote size } SEEDS = list(range(1, 11)) def make_cfg(fund_aggr, trend_share, noise_pm, mm_qty, seed): tr = trend_share * 0.6 lev = trend_share * 0.4 noise = 1.0 - (0.25 + 0.10 + 0.05 + tr + lev) mix = {"fundamentalist": 0.25, "trend": round(tr, 4), "meanrev": 0.10, "noise": round(noise, 4), "market_maker": 0.05, "statarb": 0.0, "leveraged": round(lev, 4)} mix["noise"] = round(1.0 - sum(v for k, v in mix.items() if k != "noise"), 6) return RunConfig.from_yaml(ROOT / "configs/baseline.yaml", **{ "name": f"cal_fa{fund_aggr}_ts{trend_share}_pm{noise_pm}_mm{mm_qty}", "seed": seed, "n_ticks": 600, "population.mix": mix, "population.policy_kwargs": {"fundamentalist": {"aggressiveness": fund_aggr}, "noise": {"p_market": noise_pm}, "market_maker": {"quote_qty": mm_qty}}, "treatment": {"fund_aggr": fund_aggr, "trend_share": trend_share, "noise_pm": noise_pm, "mm_qty": mm_qty}}) def one(args): cfg = make_cfg(*args) return run_outcomes(run_config(cfg)) if __name__ == "__main__": jobs = [(*g, s) for g in itertools.product(*GRID.values()) for s in SEEDS] print(f"{len(jobs)} runs") with ProcessPoolExecutor(max_workers=8) as ex: rows = list(ex.map(one, jobs, chunksize=4)) df = pd.DataFrame(rows) out = ROOT / "results/calibration" out.mkdir(parents=True, exist_ok=True) df.to_parquet(out / "sweep.parquet", index=False) keys = ["tr_fund_aggr", "tr_trend_share", "tr_noise_pm", "tr_mm_qty"] cols = ["mad_pct", "acf_r_1", "vr5", "kurtosis", "acf_absr_1_10", "vol_bp", "spread_bp", "depth5", "lambda_bp_100", "n_bubbles", "max_dd", "time_to_absorb", "response_ratio", "volume", "pnl_market_maker"] summ = df.groupby(keys)[cols].mean().round(3) summ.to_csv(out / "sweep_summary.csv") print(summ.to_string())