1"""Phase 6 (scaling arm) — build the frozen decision library from recorded live LLM decisions and replay it2at N = 1,000 traders with zero API calls. The library policy is a k-NN replay of recorded decisions (non-adaptive);3runs are labelled provider = 'library' and reported separately from online runs."""4from __future__ import annotations56import sys7from pathlib import Path89import numpy as np10import pandas as pd1112ROOT = Path(__file__).resolve().parents[1]13sys.path.insert(0, str(ROOT / "src"))14from market_agents.experiments.config import RunConfig # noqa: E40215from market_agents.experiments.runner import run_config # noqa: E40216from market_agents.llm.library import DecisionLibrary, FEATURES # noqa: E40217from market_agents.metrics.outcomes import run_outcomes # noqa: E4021819SRC = ROOT / "results/runs/confirmatory_openai"20LIB = ROOT / "results/library/gpt-5.4-nano_single.npz"21OUT = ROOT / "results/runs/library_scale"222324def build() -> DecisionLibrary:25 run_dirs = [p.parent for p in SRC.glob("dose_*/seed*/llm_decisions.parquet")]26 if not run_dirs:27 raise SystemExit("no confirmatory runs with LLM decisions yet")28 lib = DecisionLibrary.from_runs(run_dirs)29 LIB.parent.mkdir(parents=True, exist_ok=True)30 lib.save(LIB)31 print(f"library: {len(lib)} recorded decisions from {len(run_dirs)} runs; features {FEATURES}")32 return lib333435def replay(seeds=(301, 302, 303, 304, 305), n_traders=1000, shares=(0.0, 0.25)):36 rows = []37 for n in (100, n_traders):38 act = {k: v * 100.0 / n for k, v in RunConfig().population.activity.items()}39 for share in shares:40 for s in seeds:41 cfg = RunConfig.from_yaml(ROOT / "configs/baseline.yaml", **{42 "name": f"library_scale/n{n}_share{int(share * 100)}", "seed": s, "population.n_traders": n, "population.activity": act,43 "population.llm_share": share, "population.llm.provider_mix": {"library": 1.0}, "population.llm.models": {"library": "knn-replay"},44 "population.llm.frozen_library": str(LIB), "population.llm.usd_cap": 0.0, "population.llm.cache": False,45 "treatment": {"arm": "library", "share": share, "n": n}})46 res = run_config(cfg, out_dir=OUT / f"n{n}_share{int(share * 100)}" / f"seed{s:04d}")47 oc = run_outcomes(res)48 oc["run_dir"] = str(OUT / f"n{n}_share{int(share * 100)}" / f"seed{s:04d}")49 rows.append(oc)50 print(f" n={n} share={share} seed={s}: mad={oc['mad_pct']:.2f} spread={oc['spread_bp']:.1f} bubbles={oc['n_bubbles']} llm_vol={oc['llm_share_volume']:.3f} wall={oc['wall_s']:.1f}s", flush=True)51 df = pd.DataFrame(rows)52 OUT.mkdir(parents=True, exist_ok=True)53 df.to_parquet(OUT / "outcomes.parquet", index=False)54 print(df.groupby("name")[["mad_pct", "spread_bp", "n_bubbles", "max_dd", "llm_share_volume"]].mean().round(3))555657if __name__ == "__main__":58 build()59 replay()60