import numpy as np import pandas as pd import pytest from market_agents.experiments.config import RunConfig from market_agents.experiments.population import build_population from market_agents.experiments.runner import run_config from market_agents.utils.rng import make_streams def test_end_to_end_classical_100_agents(tmp_path): cfg = RunConfig(name="t", seed=3, n_ticks=120, warmup=20) res = run_config(cfg, out_dir=tmp_path / "run") assert len(res.ticks) == 120 and res.manifest["n_trades"] > 50 assert (tmp_path / "run" / "manifest.json").exists() and (tmp_path / "run" / "ticks.parquet").exists() # accounting: total wealth change equals dividends paid minus fees (mark-to-market at the same price) assert res.agents["final_wealth"].notna().all() def test_determinism_across_runs(): cfg = RunConfig(name="t", seed=11, n_ticks=100) a, b = run_config(cfg), run_config(cfg) pd.testing.assert_frame_equal(a.ticks, b.ticks) pd.testing.assert_frame_equal(a.trades, b.trades) def test_common_random_numbers_across_llm_share(): """Matched pairs: fundamentals and news paths are identical across llm_share treatments.""" base = RunConfig(name="t", seed=5, n_ticks=80) tr = base.with_overrides(**{"population.llm_share": 0.2, "population.llm.provider_mix": {"mock": 1.0}}) a, b = run_config(base), run_config(tr) assert np.allclose(a.ticks["full_value"], b.ticks["full_value"]) sa, sb = build_population(base, make_streams(5)), build_population(tr, make_streams(5)) # non-LLM slots keep their endowments and latencies for x, y in zip(sa, sb): assert x.cash == y.cash and x.latency == y.latency assert sum(s.policy_name == "llm" for s in sb) == 20 def test_mock_llm_run_reports_shares_and_validity(): cfg = RunConfig(name="t", seed=7, n_ticks=120, population={"llm_share": 0.5, "llm": {"provider_mix": {"mock": 1.0}, "decision_interval": 10}}) res = run_config(cfg, mock_kwargs={"invalid_rate": 0.2}) m = res.manifest assert m["llm_share_traders"] == pytest.approx(0.5, abs=0.06) # market makers are never replaced assert 0 < m["llm_share_orders"] < 1 and m["n_llm_decisions"] > 100 assert 0.6 < m["llm_valid_rate"] < 0.95 assert set(res.llm["provider"]) == {"mock"} def test_zero_trade_edge_case(): cfg = RunConfig(name="t", seed=1, n_ticks=30, population={"mix": {"market_maker": 1.0}, "activity": {"market_maker": 0.0}}) res = run_config(cfg) assert res.manifest["n_trades"] == 0 and len(res.ticks) == 30 and res.ticks["mid"].notna().all() def test_shocks_are_logged_and_applied(): cfg = RunConfig(name="t", seed=2, n_ticks=60, shocks=[{"time": 10, "kind": "fundamental_jump", "size": 30.0}, {"time": 20, "kind": "liquidity"}, {"time": 30, "kind": "placebo"}]) res = run_config(cfg) assert [s["shock"] for s in res.manifest["shocks"]] == ["fundamental_jump", "liquidity", "placebo"] assert res.ticks.loc[res.ticks.t == 10, "x"].iloc[0] > res.ticks.loc[res.ticks.t == 9, "x"].iloc[0] + 25