"""Phase 6 (scaling arm) — build the frozen decision library from recorded live LLM decisions and replay it at N = 1,000 traders with zero API calls. The library policy is a k-NN replay of recorded decisions (non-adaptive); runs are labelled provider = 'library' and reported separately from online runs.""" from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) from market_agents.experiments.config import RunConfig # noqa: E402 from market_agents.experiments.runner import run_config # noqa: E402 from market_agents.llm.library import DecisionLibrary, FEATURES # noqa: E402 from market_agents.metrics.outcomes import run_outcomes # noqa: E402 SRC = ROOT / "results/runs/confirmatory_openai" LIB = ROOT / "results/library/gpt-5.4-nano_single.npz" OUT = ROOT / "results/runs/library_scale" def build() -> DecisionLibrary: run_dirs = [p.parent for p in SRC.glob("dose_*/seed*/llm_decisions.parquet")] if not run_dirs: raise SystemExit("no confirmatory runs with LLM decisions yet") lib = DecisionLibrary.from_runs(run_dirs) LIB.parent.mkdir(parents=True, exist_ok=True) lib.save(LIB) print(f"library: {len(lib)} recorded decisions from {len(run_dirs)} runs; features {FEATURES}") return lib def replay(seeds=(301, 302, 303, 304, 305), n_traders=1000, shares=(0.0, 0.25)): rows = [] for n in (100, n_traders): act = {k: v * 100.0 / n for k, v in RunConfig().population.activity.items()} for share in shares: for s in seeds: cfg = RunConfig.from_yaml(ROOT / "configs/baseline.yaml", **{ "name": f"library_scale/n{n}_share{int(share * 100)}", "seed": s, "population.n_traders": n, "population.activity": act, "population.llm_share": share, "population.llm.provider_mix": {"library": 1.0}, "population.llm.models": {"library": "knn-replay"}, "population.llm.frozen_library": str(LIB), "population.llm.usd_cap": 0.0, "population.llm.cache": False, "treatment": {"arm": "library", "share": share, "n": n}}) res = run_config(cfg, out_dir=OUT / f"n{n}_share{int(share * 100)}" / f"seed{s:04d}") oc = run_outcomes(res) oc["run_dir"] = str(OUT / f"n{n}_share{int(share * 100)}" / f"seed{s:04d}") rows.append(oc) 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) df = pd.DataFrame(rows) OUT.mkdir(parents=True, exist_ok=True) df.to_parquet(OUT / "outcomes.parquet", index=False) print(df.groupby("name")[["mad_pct", "spread_bp", "n_bubbles", "max_dd", "llm_share_volume"]].mean().round(3)) if __name__ == "__main__": build() replay()