SPB Git

spb/wp27_uqo

Public

WP27: exchange engine, agents, LLM adapters, designs, theory, literature, pre-analysis plan (pre-pilot freeze)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 27, 2026) Browse files

113 changed files +10,847 −0

113 changed files
Unified Split
added .env.example +5 −0
@@ -0,0 +1,5 @@
1+# Copy to .env (never committed). Keys are read by src/market_agents/llm/providers/*.py via os.environ only.
2+OPENAI_API_KEY=sk-...
3+ANTHROPIC_API_KEY=sk-ant-...
4+# Optional overrides (defaults live in configs/*.yaml)
5+# MARKET_AGENTS_LLM_USD_CAP=40
added .gitignore +46 −0
@@ -0,0 +1,46 @@
1+# Raw & processed data — never committed (see data/raw/README.md for provenance)
2+data/raw/*
3+!data/raw/README.md
4+data/processed/*
5+!data/processed/.gitkeep
6+
7+# Python
8+__pycache__/
9+*.pyc
10+.pytest_cache/
11+.venv/
12+*.egg-info/
13+
14+# LaTeX build artifacts (paper/main.pdf IS committed)
15+paper/*.aux
16+paper/*.log
17+paper/*.out
18+paper/*.toc
19+paper/*.lof
20+paper/*.lot
21+paper/*.fls
22+paper/*.fdb_latexmk
23+paper/*.bbl
24+paper/*.blg
25+paper/*.spl
26+paper/*.synctex.gz
27+paper/*.run.xml
28+paper/*.bcf
29+
30+# Logs
31+logs/
32+*.log
33+
34+# macOS
35+.DS_Store
36+
37+# WP27 — secrets, run artefacts, caches
38+.env
39+results/runs/*
40+!results/runs/.gitkeep
41+results/cache/
42+logs/
43+*.duckdb
44+*.parquet
45+uv.lock.bak
46+paper/main_anon.pdf
added Makefile +57 −0
@@ -0,0 +1,57 @@
1+# UQO WP 27 — The Invisible Hand of Machines. One-command no-API replication: `make replicate`.
2+PY ?= .venv/bin/python
3+W ?= 6
4+
5+.PHONY: all setup test smoke calibrate scale classical pilot confirmatory library figures tables paper replicate clean
6+
7+all: test classical figures tables paper
8+
9+setup:
10+ uv sync --extra dev
11+
12+test:
13+ $(PY) -m pytest tests -q
14+
15+smoke: # 100 agents, fixed seed, zero paid calls, accounting verified
16+ $(PY) scripts/smoke_test.py
17+
18+calibrate:
19+ $(PY) scripts/10_calibration_sweep.py
20+
21+scale:
22+ $(PY) scripts/11_scale_benchmark.py
23+
24+classical: # 16 cells x 30 matched seeds, no API
25+ $(PY) -m market_agents.cli design configs/classical_confirmatory.yaml --workers $(W) --root results/runs
26+ $(PY) -m market_agents.cli design configs/exploratory.yaml --workers $(W) --root results/runs
27+
28+pilot: # paid: ~3 USD (OpenAI). Requires OPENAI_API_KEY in .env
29+ $(PY) -m market_agents.cli budget configs/pilot_openai.yaml
30+ $(PY) -m market_agents.cli design configs/pilot_openai.yaml --workers 3 --cache results/cache/llm_cache.sqlite --root results/runs
31+
32+confirmatory: # paid: ~30 USD (OpenAI)
33+ $(PY) -m market_agents.cli budget configs/confirmatory_openai.yaml
34+ $(PY) -m market_agents.cli design configs/confirmatory_openai.yaml --workers 5 --cache results/cache/llm_cache.sqlite --root results/runs
35+
36+library: # frozen decision library from recorded runs + replay at N = 1,000 (no API)
37+ $(PY) scripts/60_frozen_library.py
38+
39+theory:
40+ $(PY) scripts/90_theory_checks.py
41+
42+figures:
43+ $(PY) scripts/40_figures.py
44+
45+tables:
46+ $(PY) scripts/50_tables.py
47+ mkdir -p paper/figures paper/tables
48+ cp results/figures/*.pdf paper/figures/
49+ cp results/tables/*.tex paper/tables/
50+
51+paper: tables
52+ cd paper && latexmk -pdf main.tex
53+
54+replicate: test smoke classical theory figures tables # everything that needs no API key
55+
56+clean:
57+ cd paper && latexmk -C
added README.md +3 −0
@@ -0,0 +1,3 @@
1+# The Invisible Hand of Machines
2+
3+(README written at the end of the build — see Makefile.)
added configs/baseline.yaml +49 −0
@@ -0,0 +1,49 @@
1+# Baseline classical economy (no LLM). All numbers documented in docs/assumptions.md.
2+name: baseline
3+seed: 1
4+n_ticks: 600
5+warmup: 100
6+assets: {n_assets: 1, tick_cents: 1, initial_price_cents: 20000, price_band_pct: 0.5, max_order_qty: 10000}
7+fundamentals: {div_interval: 20, r_period: 0.0025, mu: [50.0], mu_shift: 0.0, p_switch: 0.0, rho: 0.98, sigma: 2.0,
8+ jump_prob: 0.0, jump_sigma: 0.0, factor_loading: [0.0], x0: 0.0}
9+news: {rate: 0.02, noise_sd: 1.0, misleading_share: 0.0, misleading_bias: 10.0, window: 20}
10+exchange: {maker_fee_bps: 0.0, taker_fee_bps: 2.0, min_latency: 0.05, halt_threshold: null, halt_window: 10, halt_duration: 20,
11+ halt_cancel_resting: true, allow_short: false, max_short_lots: 0, margin_check: true, liquidation_fraction: 0.5, order_ttl: 50}
12+population:
13+ n_traders: 100
14+ mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.50, market_maker: 0.05, statarb: 0.0, leveraged: 0.04} # calibrated 2026-10-27 (scripts/10)
15+ activity: {fundamentalist: 0.20, trend: 0.20, meanrev: 0.20, noise: 0.30, market_maker: 0.5, statarb: 0.2, leveraged: 0.2}
16+ policy_kwargs: {fundamentalist: {aggressiveness: 20.0}, noise: {p_market: 0.6}, market_maker: {quote_qty: 20}}
17+ llm_share: 0.0
18+ llm_replace: proportional
19+ llm_keep_market_makers: true
20+ informed_share: 0.3
21+ precision: 2.0
22+ leverage_cap: 3.0
23+ maintenance_margin: 0.25
24+ endowment: {cash_cents: 2000000, shares: 100, wealth_dispersion: 0.3}
25+ latency: {min: 0.05, max: 1.0, llm_latency: null}
26+ llm:
27+ provider_mix: {mock: 1.0}
28+ models: {mock: mock-v1, openai: gpt-5.4-nano, anthropic: claude-haiku-5-5}
29+ prompt_set: single
30+ prompt_version: v1
31+ memory: 0
32+ channel: false
33+ cohort_size: 5
34+ decision_interval: 20
35+ stagger: true
36+ stagger_slots: 4
37+ informed_share: 0.3
38+ precision: 2.0
39+ max_order_qty: 50
40+ reasoning_effort: low
41+ max_output_tokens: 1200
42+ concurrency: 8
43+ usd_cap: 5.0
44+ cache: true
45+shocks:
46+ - {time: 300, kind: fundamental_jump, size: 10.0} # +10 cents on the latent deviation -> +10/(1+r-rho) = +4.4 on V (+2.2 %), revealed only through news / the next dividend
47+ - {time: 450, kind: placebo}
48+output: {events: false, trades: true, llm_log: true, lookback: 60}
49+treatment: {}
added configs/classical_confirmatory.yaml +24 −0
@@ -0,0 +1,24 @@
1+# Classical (no-API) confirmatory design: mechanism checks that do not need an LLM — 30 matched seeds.
2+# Used for (i) stylized-fact validation, (ii) the mock "shared-model" knob (herd_bias) as a controlled analogue of
3+# correlated errors, (iii) safeguards and leverage comparative statics predicted by Props 4–6, (iv) placebo controls.
4+name: classical_confirmatory
5+base: baseline.yaml
6+seeds: {start: 101, n: 30}
7+overrides: {population.llm.provider_mix: {mock: 1.0}, population.llm.usd_cap: 0.0}
8+cells:
9+ - {name: control, overrides: {}, treatment: {arm: control}}
10+ - {name: placebo_shock, overrides: {shocks: [{time: 300, kind: placebo}, {time: 450, kind: placebo}]}, treatment: {arm: placebo}}
11+ - {name: trend_heavy, overrides: {population.mix: {fundamentalist: 0.25, trend: 0.16, meanrev: 0.10, noise: 0.40, market_maker: 0.05, statarb: 0.0, leveraged: 0.04}}, treatment: {arm: feedback, trend: 0.16}}
12+ - {name: leverage_heavy, overrides: {population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.35, market_maker: 0.05, statarb: 0.0, leveraged: 0.19}}, treatment: {arm: leverage, leveraged: 0.19}}
13+ - {name: leverage_heavy_funding_shock, overrides: {population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.35, market_maker: 0.05, statarb: 0.0, leveraged: 0.19}, shocks: [{time: 300, kind: fundamental_jump, size: -10.0}, {time: 320, kind: funding, size: 0.5}]}, treatment: {arm: leverage, leveraged: 0.19, funding_shock: 0.5}}
14+ - {name: liquidity_shock, overrides: {shocks: [{time: 300, kind: fundamental_jump, size: 10.0}, {time: 300, kind: liquidity}, {time: 360, kind: liquidity_end}]}, treatment: {arm: liquidity, mm_withdrawn: 60}}
15+ - {name: info_low, overrides: {population.precision: 0.5}, treatment: {arm: information, precision: 0.5}}
16+ - {name: info_high, overrides: {population.precision: 8.0}, treatment: {arm: information, precision: 8.0}}
17+ - {name: misleading_news, overrides: {news.misleading_share: 0.5}, treatment: {arm: information, misleading: 0.5}}
18+ - {name: breaker_5, overrides: {exchange.halt_threshold: 0.05}, treatment: {arm: safeguard, breaker: 0.05}}
19+ - {name: poslimit_300, overrides: {exchange.position_limit: 300}, treatment: {arm: safeguard, position_limit: 300}}
20+ - {name: auction_5, overrides: {exchange.call_auction_interval: 5}, treatment: {arm: mechanism, auction: 5}}
21+ - {name: mock_llm_25_indep, overrides: {population.llm_share: 0.25}, treatment: {arm: mockherd, share: 0.25, herd: 0.0}}
22+ - {name: mock_llm_50_indep, overrides: {population.llm_share: 0.50}, treatment: {arm: mockherd, share: 0.50, herd: 0.0}}
23+ - {name: k2_control, overrides: {assets.n_assets: 2, fundamentals.mu: [50.0, 50.0], fundamentals.factor_loading: [0.7, 0.7], population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.45, market_maker: 0.05, statarb: 0.05, leveraged: 0.04}}, treatment: {arm: contagion, assets: 2}}
24+ - {name: k2_leverage, overrides: {assets.n_assets: 2, fundamentals.mu: [50.0, 50.0], fundamentals.factor_loading: [0.7, 0.7], population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.30, market_maker: 0.05, statarb: 0.05, leveraged: 0.19}, shocks: [{time: 300, kind: fundamental_jump, size: -10.0, asset: 0}, {time: 320, kind: funding, size: 0.5}]}, treatment: {arm: contagion, assets: 2, leveraged: 0.19, funding_shock: 0.5}}
added configs/confirmatory_openai.yaml +30 −0
@@ -0,0 +1,30 @@
1+# Phase 6 confirmatory design (live OpenAI; Anthropic cells blocked by the invalid key — see docs/model_cards.md).
2+# Matched seeds 201–207 (7 seeds, 80 % power for paired d ≈ 1.2; all primary outcomes Holm-adjusted). Frozen 2026-10-27
3+# after the pilot (same code revision as the pilot except the pre-registered bug fixes recorded in docs/research_log.md).
4+name: confirmatory_openai
5+base: baseline.yaml
6+seeds: {start: 201, n: 7}
7+overrides: {population.llm.provider_mix: {openai: 1.0}, population.llm.models: {openai: gpt-5.4-nano, openai_mini: gpt-5.4-mini, mock: mock-v1, anthropic: claude-haiku-5-5},
8+ population.llm.usd_cap: 2.0, population.llm.concurrency: 8}
9+cells:
10+ # --- A. dose–response in the share of LLM-controlled trader slots (single generalist prompt, one model) ---
11+ - {name: dose_000, overrides: {population.llm_share: 0.00}, treatment: {arm: dose, share: 0.00, model: gpt-5.4-nano, prompts: single}}
12+ - {name: dose_005, overrides: {population.llm_share: 0.05}, treatment: {arm: dose, share: 0.05, model: gpt-5.4-nano, prompts: single}}
13+ - {name: dose_010, overrides: {population.llm_share: 0.10}, treatment: {arm: dose, share: 0.10, model: gpt-5.4-nano, prompts: single}}
14+ - {name: dose_025, overrides: {population.llm_share: 0.25}, treatment: {arm: dose, share: 0.25, model: gpt-5.4-nano, prompts: single}}
15+ - {name: dose_050, overrides: {population.llm_share: 0.50}, treatment: {arm: dose, share: 0.50, model: gpt-5.4-nano, prompts: single}}
16+ - {name: dose_100, overrides: {population.llm_share: 1.00}, treatment: {arm: dose, share: 1.00, model: gpt-5.4-nano, prompts: single}}
17+ # --- B. diversity at 25 %: same model + diverse persona prompts; two-model mix (nano + mini) with diverse prompts ---
18+ - {name: div_prompts_025, overrides: {population.llm_share: 0.25, population.llm.prompt_set: diverse}, treatment: {arm: diversity, share: 0.25, model: gpt-5.4-nano, prompts: diverse}}
19+ - {name: div_models_025, overrides: {population.llm_share: 0.25, population.llm.prompt_set: diverse, population.llm.provider_mix: {openai: 0.5, openai_mini: 0.5}}, treatment: {arm: diversity, share: 0.25, model: mix-nano-mini, prompts: diverse}}
20+ # --- C. cognition channels at 25 %: bounded memory, communication channel, adaptive P&L feedback ---
21+ - {name: memory_025, overrides: {population.llm_share: 0.25, population.llm.memory: 5}, treatment: {arm: cognition, share: 0.25, memory: 5}}
22+ - {name: channel_025, overrides: {population.llm_share: 0.25, population.llm.channel: true}, treatment: {arm: cognition, share: 0.25, channel: true}}
23+ - {name: adaptive_025, overrides: {population.llm_share: 0.25, population.llm.adaptive_prompt: true}, treatment: {arm: cognition, share: 0.25, adaptive: true}}
24+ # --- D. interaction with leverage and safeguards at 25 % ---
25+ - {name: leverage_025, overrides: {population.llm_share: 0.25, population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.35, market_maker: 0.05, statarb: 0.0, leveraged: 0.19}}, treatment: {arm: leverage, share: 0.25, leveraged: 0.19}}
26+ - {name: breaker_025, overrides: {population.llm_share: 0.25, exchange.halt_threshold: 0.05}, treatment: {arm: safeguard, share: 0.25, breaker: 0.05}}
27+ - {name: poslimit_025, overrides: {population.llm_share: 0.25, exchange.position_limit: 300}, treatment: {arm: safeguard, share: 0.25, position_limit: 300}}
28+ # --- E. fairness: equal USD ≈ 25 % nano vs 5 % mini (mini costs ~5x per decision); matched latency ---
29+ - {name: fair_mini_005, overrides: {population.llm_share: 0.05, population.llm.provider_mix: {openai_mini: 1.0}}, treatment: {arm: fairness, share: 0.05, model: gpt-5.4-mini, prompts: single}}
30+ - {name: fair_latency_025, overrides: {population.llm_share: 0.25, population.latency.llm_latency: 0.05}, treatment: {arm: fairness, share: 0.25, llm_latency: 0.05}}
added configs/exploratory.yaml +18 −0
@@ -0,0 +1,18 @@
1+# Phase 5 pilot design — every cell first dry-run with the mock provider (this file), then live (pilot_openai.yaml).
2+name: exploratory_mock
3+base: baseline.yaml
4+seeds: {start: 1, n: 3}
5+overrides: {population.llm.provider_mix: {mock: 1.0}, population.llm.usd_cap: 0.0}
6+cells:
7+ - {name: share_0, overrides: {population.llm_share: 0.0}, treatment: {arm: dose, share: 0.0}}
8+ - {name: share_5, overrides: {population.llm_share: 0.05}, treatment: {arm: dose, share: 0.05}}
9+ - {name: share_25, overrides: {population.llm_share: 0.25}, treatment: {arm: dose, share: 0.25}}
10+ - {name: share_50, overrides: {population.llm_share: 0.50}, treatment: {arm: dose, share: 0.50}}
11+ - {name: diverse_25, overrides: {population.llm_share: 0.25, population.llm.prompt_set: diverse}, treatment: {arm: diversity, share: 0.25, prompts: diverse}}
12+ - {name: memory_25, overrides: {population.llm_share: 0.25, population.llm.memory: 5}, treatment: {arm: memory, share: 0.25, memory: 5}}
13+ - {name: channel_25, overrides: {population.llm_share: 0.25, population.llm.channel: true}, treatment: {arm: channel, share: 0.25, channel: true}}
14+ - {name: leverage_25, overrides: {population.llm_share: 0.25, population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.35, market_maker: 0.05, statarb: 0.0, leveraged: 0.19}}, treatment: {arm: leverage, share: 0.25, leveraged: 0.19}}
15+ - {name: breaker_25, overrides: {population.llm_share: 0.25, exchange.halt_threshold: 0.05}, treatment: {arm: safeguard, share: 0.25, breaker: 0.05}}
16+ - {name: poslimit_25, overrides: {population.llm_share: 0.25, exchange.position_limit: 300}, treatment: {arm: safeguard, share: 0.25, position_limit: 300}}
17+ - {name: auction_25, overrides: {population.llm_share: 0.25, exchange.call_auction_interval: 5}, treatment: {arm: mechanism, share: 0.25, auction: 5}}
18+ - {name: k2_25, overrides: {population.llm_share: 0.25, assets.n_assets: 2, fundamentals.mu: [50.0, 50.0], fundamentals.factor_loading: [0.7, 0.7], population.mix: {fundamentalist: 0.25, trend: 0.06, meanrev: 0.10, noise: 0.45, market_maker: 0.05, statarb: 0.05, leveraged: 0.04}}, treatment: {arm: contagion, share: 0.25, assets: 2}}
added configs/pilot_openai.yaml +10 −0
@@ -0,0 +1,10 @@
1+# Phase 5 live pilot (OpenAI only — the Anthropic key was rejected on 2026-10-27). Hard cap 5 USD.
2+name: pilot_openai
3+base: baseline.yaml
4+seeds: {start: 1, n: 3}
5+overrides: {population.llm.provider_mix: {openai: 1.0}, population.llm.models: {openai: gpt-5.4-nano, mock: mock-v1, anthropic: claude-haiku-5-5},
6+ population.llm.usd_cap: 1.5, population.llm.concurrency: 8}
7+cells:
8+ - {name: share_5, overrides: {population.llm_share: 0.05}, treatment: {arm: dose, share: 0.05, provider: openai, model: gpt-5.4-nano}}
9+ - {name: share_25, overrides: {population.llm_share: 0.25}, treatment: {arm: dose, share: 0.25, provider: openai, model: gpt-5.4-nano}}
10+ - {name: share_50, overrides: {population.llm_share: 0.50}, treatment: {arm: dose, share: 0.50, provider: openai, model: gpt-5.4-nano}}
added data/processed/.gitkeep +0 −0
added docs/DESIGN-BRIEF.md +59 −0
@@ -0,0 +1,59 @@
1+# Design brief — The Invisible Hand of Machines (UQO WP 27)
2+
3+Internal brief shared by the author and the sub-agents writing the literature review, theory and
4+pre-analysis plan. It describes what the simulator *actually* implements (src/market_agents).
5+
6+## Economy implemented in the simulator
7+- Discrete ticks t = 0..T-1 (T = 300–1000). One risky asset (optionally K = 2–5 with a common factor in
8+ dividend innovations) and cash. Prices in integer ticks (1 cent), quantities in integer lots, cash in
9+ integer cents; exact conservation checked after every fill.
10+- Dividend every `div_interval` = 20 ticks: D = mu_{s} + x + J, x_t = rho x_{t-1} + sigma eps_t (rho = 0.98,
11+ sigma = 2 cents, mu = 50 cents), optional two-state regime in mu (Markov), optional jumps.
12+ Latent fundamental value V_t = mu/r + x_t rho/(1+r-rho) with r = 0.25 % per period (so P/D ≈ 400,
13+ V ≈ 200.00). Public value V^pub_t = same formula on the Kalman/HMM posterior from announced dividends
14+ and public news. Agents never see V_t.
15+- Information: public news items (noisy reports of x_t, some deliberately biased = misleading);
16+ private signals s_i = x_t + N(0, sigma²/precision) for "informed" agents (fundamentalists and a share of
17+ LLM agents). No agent sees future draws.
18+- Exchange: continuous double auction, price–time priority, limit/market/cancel, partial fills, IOC
19+ market remainder, taker fee 2 bp, maker fee 0, per-agent latency in [0.05, 1] tick (orders arrive
20+ at decision time + latency and are processed in arrival order), order TTL 50 ticks, price band ±50 %,
21+ optional circuit breaker (halt when |10-tick move| > threshold), no short selling by default,
22+ leverage cap 3× for "leveraged" slots with maintenance margin 25 % and forced partial liquidation.
23+- Population (N = 100 / 1,000 / 10,000 trader slots): fundamentalists 25 % (30 % informed, precision 2),
24+ trend followers 15 %, mean-reverters 10 %, noise traders 35 %, market makers 5 % (bounded
25+ inventory-skewed two-sided quotes), leveraged trend followers 10 %, cross-asset arbitrageurs (multi-asset
26+ only). Endowments: cash 20,000 + 100 shares scaled by a lognormal wealth multiplier (sd 0.3).
27+- LLM treatment: a fraction `llm_share` of trader *slots* (stratified across policy types, never market
28+ makers) is replaced by an LLM policy that decides every 20 ticks (staggered offsets) from a canonical
29+ snapshot (book depth 5 levels, mid history, recent trades, last dividend, public PV, news, own private
30+ signal if any, own account and open orders). Output = strictly validated JSON (hold/place/cancel …).
31+ Invalid decisions = hold, logged. Realized LLM share of agents, orders and volume are all reported.
32+ Providers: OpenAI (Responses API, json_schema), Anthropic (Messages API, json_schema), mock (seeded
33+ rule with injected invalid answers). Treatments: provider mix, same vs diverse persona prompts,
34+ memory (0 vs 5 past decisions), communication channel (cohorts of 5 share rationales), adaptive
35+ P&L feedback, matched latency, equal token / USD / decision-opportunity budgets, frozen decision
36+ library replay vs online calls.
37+- Randomization: master seed → independent streams (fundamentals, news, slots, policy assignment, LLM
38+ slot assignment, activation, tie-breaks, per-agent RNG). Matched pairs across `llm_share` share
39+ every stream except the LLM slot draw. ≥ 30 seeds exploratory, power-based for confirmatory.
40+
41+## Stylized theoretical model (to be written up by the theory agent, must map to the above)
42+One-period, one asset, liquidation value v ~ N(0, σ_v²) around the public PV. N traders receive
43+s_i = v + ε_i with ε_i = η_{g(i)} + u_i, where g(i) is the trader's "model" (foundation model /
44+prompt family), η_g ~ N(0, σ_η²) iid across models, u_i ~ N(0, σ_u²) iid. A share φ_g of traders uses
45+model g (Σ_g φ_g ≤ 1; the remaining 1 − Σφ_g traders have fully idiosyncratic errors, i.e. their own
46+private model). Traders submit market orders x_i = β s_i (β > 0 a fixed response intensity; the
47+behavioral assumption mirrors rule-like LLM policies and is relaxed in a CARA extension). Noise
48+traders submit z ~ N(0, σ_z²). A competitive, risk-neutral market maker sets P = E[v | Y],
49+Y = Σ_i x_i + z, so P = λ Y with λ = Cov(v,Y)/Var(Y).
50+Objects: price informativeness τ = 1 − Var(v | Y)/σ_v²; pricing error Var(P − v); order-flow
51+correlation ρ_x = Corr(x_i, x_j); synchrony; effective number of models M_eff = 1/Σ_g φ_g²;
52+feedback extension with trend demand γ(P_1 − P_0) and amplification 1/(1 − λγ); leverage extension
53+with margin m, wealth W, impact λ: amplification 1/(1 − λ X/m) and spiral when λ X/m ≥ 1, where X is
54+the aggregate same-signed leveraged position (increasing in the shared-model share).
55+Hypotheses the simulator tests: H1 informativeness falls in the share using a single shared model
56+(holding information quality fixed) and rises in M_eff; H2 order-flow correlation and synchrony rise
57+in φ; H3 bubble/crash frequency and liquidity spirals rise with φ × leverage; H4 prompt/model
58+diversity restores part of the loss; H5 communication channels raise synchrony; H6 circuit breakers
59+and position limits cut crash depth at a cost in time-to-absorb.
added docs/assumptions.md +50 −0
@@ -0,0 +1,50 @@
1+# Assumption register — WP 27
2+
3+Version 1.0, 2026-10-27. Each row states the assumption *as implemented* in `src/market_agents`, its role, why it is acceptable for the research question, and the pre-specified check that probes it. Values are the defaults of `configs/baseline.yaml` unless noted. "Confirmatory base" refers to the overrides in `protocol.md` §5.1.
4+
5+## 1. Time, assets and fundamentals
6+
7+| ID | Assumption (as implemented) | Role | Justification | Robustness check |
8+|---|---|---|---|---|
9+| A1 | Discrete model time: 600 ticks, outcomes on t ≥ 100; no calendar mapping | Defines the horizon of every metric | Avoids false precision; 30 dividend periods suffice for ACF/VR estimates (≥ 500 returns) | `n_ticks` 1,000 on the control (free); report whether rankings of cells change |
10+| A2 | Dividend every 20 ticks: D = μ + x + J, x_t = 0.98 x_{t−1} + 2 ε_t (cents), μ = 50 c, r = 0.25 % per period; one asset by default | Fundamental process; V ≈ 200.00 and P/D ≈ 400 | Persistent, stationary cash-flow state with a closed-form PV (brief §Economy) | Sweep ρ ∈ {0.95, 0.98}, σ ∈ {1, 2, 4} on the control (free) |
11+| A3 | **"Efficient price" = full-information PV** V_t = μ/r + x_t·ρ/(1 + r − ρ), risk-neutral, constant discount rate, no risk premium | Benchmark for `msd_log`, `mad_pct`, bubbles, absorption | The only unambiguous benchmark in a simulated economy; the latent state is never shown to agents | Also report deviations from the *public* PV (`public_value`) and from the end-of-run mean price |
12+| A4 | Public PV = Kalman/HMM posterior on announced dividends; in the baseline (no jumps, one regime) the dividend reveals x_t exactly, so V^pub ≡ V | Information structure | Simplest case for calibration; **but it makes private signals redundant and H1 unidentified** | Confirmatory base uses noisy dividends (`jump_prob` 1, `jump_sigma` ≈ 4 c → SD(V^pub/V − 1) ≈ 0.5 %); both variants reported |
13+| A5 | No look-ahead: private signals s = x_t + N(0, σ²/precision) are about the *current* state; news reports x_t with noise; nobody sees future draws | Rules out mechanical information advantages | Standard in noisy rational-expectations settings | Unit test on the signal function; prompt audit (`prompting.py` builds only from `Observation`) |
14+| A6 | Shock: latent jump in x at t = 300 revealed through dividends/news; placebo at t = 450. Baseline size 40 c (+8.4 % on V); confirmatory size 10 c (+2.1 %) | Event-study outcomes (`time_to_absorb`, `response_ratio`, local projections) | A sub-threshold (< 5 %) jump so that a lagged response is not counted as a bubble; placebo for falsification | Sizes 10/20/40 c on the control; `news_burst` variant |
15+
16+## 2. Market mechanism
17+
18+| ID | Assumption | Role | Justification | Robustness check |
19+|---|---|---|---|---|
20+| A7 | Continuous double auction, price–time priority, limit/market/cancel, partial fills, IOC remainder of market orders, integer ticks (1 c) and lots | Price formation; no Walrasian auctioneer and no imposed equilibrium | Matches modern electronic markets; prices emerge from order matching | `call_auction_interval` = 5 (batch clearing with a 2 % collar), mock only |
21+| A8 | Per-agent latency U[0.05, 1] tick, fixed for the run; orders processed in arrival order; LLM agents draw latency like others | Removes a speed advantage from either population | Speed is not the object of study | `latency.llm_latency` matched-latency variant |
22+| A9 | Fees: taker 2 bp, maker 0, no rebates; fees leave the economy; exact cash/share conservation asserted after every fill | Transaction costs; accounting integrity | Low explicit costs so that spreads are endogenous | Taker 0 and 5 bp on the control |
23+| A10 | Order TTL 50 ticks; price band ±50 % of the reference; max order 10,000 lots (policies cap at 40–50) | Keeps books finite and prices bounded | Prevents stale quotes and absurd limit prices | TTL 20 / 100 on the control |
24+| A11 | No short selling by default; buys limited by free cash (reserved for resting buys) | Budget constraint; asymmetry between buying and selling | Retail-like constraint; makes the no-short assumption explicit for H3 | `allow_short` with `max_short_lots` 100 on the control and at φ = 0.25 (mock) |
25+| A12 | Leverage only for "leveraged" slots: cap 3× gross/equity, maintenance margin 25 %, forced sale of 50 % by market order on the next tick, bankrupt agents frozen | The leverage channel of H3 (spiral when λX/m ≥ 1) | Mirrors margin trading with mechanical liquidation | Cap 2×/5×; maintenance 15 %/35 % (free at φ = 0, live only in the T4 leverage cell) |
26+| A13 | Safeguards off by default; treatments: halt when |10-tick move| > 5 % for 20 ticks with resting orders cancelled; position limit 300 lots | H6 | Simple, transparent rules | Halt 3 %/8 %; limit 150/600 (mock) |
27+| A14 | Market making by 5 bounded inventory-skewed two-sided quoters (half-spread 8 ticks + volatility term, skew 2 % at 200 lots), endowed ×5, never replaced by LLM agents | Liquidity supply; spread floor ≈ 8 bp | Keeps a stable liquidity benchmark across cells so liquidity changes come from the treated population | `quote_qty` 10/20 and `liquidity` shock (makers withdraw) on the control |
28+
29+## 3. Agents
30+
31+| ID | Assumption | Role | Justification | Robustness check |
32+|---|---|---|---|---|
33+| A15 | **No rationality or equilibrium assumption**: classical agents are documented heuristics (fundamentalist with signal-weighted value, EMA trend, z-score mean reverter, zero-intelligence-plus noise, leveraged trend); LLM agents do whatever the model outputs | Defines the "human-like" baseline | The brief's theory (β-response traders) is a stylization; the simulator tests directional claims, not point predictions | Calibration sweep (360 runs) shows the stylized facts are robust to aggressiveness, trend share, market-order share |
34+| A16 | Mix 25/15/10/35/5/10 % (fundamentalist/trend/meanrev/noise/MM/leveraged), stratified by wealth; endowment 20,000 + 100 shares × lognormal(0.3) | Population composition | Common in ABM calibrations; 50/50 cash–stock endowment at V | Trend share 10–25 % in the sweep; `wealth_dispersion` 0/0.6 |
35+| A17 | Decision frequency: classical activation Bernoulli(0.2–0.5) per tick; LLM agents decide every 20 ticks with staggered offsets, from t = 50 | Event frequency vs decision frequency | API cost; one decision per dividend period is a defensible "slow trader" | Realized order/volume shares always reported (mock: 3.5 % / 8 % at φ = 0.25); `decision_interval` 10 (mock) |
36+| A18 | Information: 30 % of fundamentalists informed (precision 2 → signal SD 1.4 c); after the gate-4 fix an LLM slot inherits the status of the slot it replaces | Holds information quality fixed across arms (H1's premise) | Otherwise the treatment bundles policy and information (mock: 44 % vs 6 % informed) | Report `stratum` composition of LLM slots per cell; variant with `llm.informed_share` 0 |
37+| A19 | Stratified replacement: proportional quotas by classical type, market makers excluded → 95 eligible slots, so φ = 1 means 95 % | Dose definition | Keeps composition of the *replaced* population constant along the dose | `llm_replace: noise` (replace one type only) at φ = 0.25 (mock) |
38+| A20 | LLM observation = canonical snapshot (5 levels of depth, 60-tick mid history, 30 recent trades, last dividend, public PV, ≤ 3 news items, own signal, account, open orders); personas single ("generalist") or 8 diverse; memory 0/5; channel = last 5 rationales of a cohort | What the model can condition on | Identical information set to the classical agents' `Observation` | Prompt audit; prompt version frozen (v1) |
39+| A21 | Invalid or infeasible decisions become `hold` (quantity, ±20 % price window, cash/holdings, owned cancel ids); sells clipped to holdings | Reliability rule; ITT estimand | A deployed agent that fails holds | Validity reported per cell; cells < 80 % flagged |
40+| A22 | Provider sampling is not seeded; cached responses replay exactly; the frozen library is a nearest-neighbour map, not an agent | Reproducibility | Only option with current APIs | Cache-hit share and online/replay flag per run; library distance diagnostics |
41+| A23 | Equal decision opportunities across models by default (27 per agent); equal-USD via per-run `usd_cap` (single-provider cells) | Fairness of cross-model comparisons | Decision opportunities, not tokens, are the economically relevant unit | T5 fairness cell |
42+
43+## 4. Measurement
44+
45+| ID | Assumption | Role | Justification | Robustness check |
46+|---|---|---|---|---|
47+| A24 | Mid-price is the price; metrics use the mid (fallback in `engine.mid_ticks` when one side is empty); effective spread uses the start-of-tick mid | All price-based outcomes | Standard; avoids bid–ask bounce | Also compute `msd_log` on last trade prices |
48+| A25 | Bubble = |P/V − 1| > 5 % for ≥ 10 ticks; crash = drop ≥ 10 % from the trailing 20-tick max; `time_to_absorb` tolerance 1 % for 5 ticks, censored at 100 | Instability and absorption outcomes | Pre-specified; the 10 % crash rule has a floor effect in the control (0 crashes) | Grid bubble 3/5/8 %, crash 7/10/15 %, computed on saved ticks |
49+| A26 | Coordination measured among LLM agents only (sign correlation in 20-tick windows; synchrony per decision round) | H2/H5 | Only LLM decisions are logged with intent | Classical analogue from aggressor-signed trades (analysis stage) |
50+| A27 | Multi-asset: common factor 0.7 in innovations; LLM prompt covers asset 0 only | Contagion arm | Keeps the prompt single-asset (cost, validity) | Report that spillover runs from the LLM-traded asset to the classical-only asset |
added docs/identification.md +191 −0
@@ -0,0 +1,191 @@
1+# Identification and pre-analysis plan — WP 27
2+
3+Version 1.0, 2026-10-27, written before any live API call. Everything in §3–§6 is frozen after the pilot (§6.2); changes afterwards are logged as deviations in the paper.
4+
5+## 1. Causal structure
6+
7+### 1.1 Variables
8+
9+- **Z — treatment (cell labels)**: `llm_share` φ ∈ {0, …, 1}; provider/model mix (`provider_mix`, `models`); `prompt_set` ∈ {single, diverse}; `memory` ∈ {0, 5}; `channel` ∈ {off, on}; leverage (share of leveraged slots, `leverage_cap`); safeguards (`halt_threshold`, `position_limit`); fairness regime (`decision_cap`, `usd_cap`). A cell is a vector of these.
10+- **U — exogenous randomness, by named seed stream** (`utils/rng.py`): `fundamentals` (dividend path, jumps), `news`, `slots` (wealth multipliers, latencies, informed draws), `assign` (classical policy of each slot), `llm_assign` (which eligible slots become LLM, provider allocation, decision offsets), `activation`, `tiebreak`, and per-agent generators `agent_rng(seed, id)` (classical policy randomness, private-signal noise).
11+- **ε — provider noise**: the LLM's sampling randomness, not seeded and not controllable (gpt-5 family rejects `temperature`; no seed parameter). It is part of the treatment.
12+- **M — within-run mediators** (`outcomes.py`): order-flow correlation ρ̄ = `llm_pairwise_corr`, synchrony = `llm_synchrony`, aggressiveness β̂ = `llm_market_share` (share of market orders) and mean order size, validity = `llm_valid_rate`, realized dose = `llm_share_orders`, `llm_share_volume`.
13+- **Y — run-level outcomes** (§3).
14+
15+### 1.2 DAG
16+
17+```
18+ U_fund, U_news ───────────────────────────────────────────┐
19+ U_slots, U_assign, U_agents ───────────────┐ │
20+ │ │
21+ cell label ─────► Z ───┐ ▼ ▼
22+ ├──► LLM slot set & policies ──► M (ρ̄, synchrony, β̂, validity, realized dose) ──► Y
23+ U_llm_assign ──────────┘ ▲ ▲ ▲
24+ │ │ │
25+ ε_provider ─────────────────────────┴─────────────────────┘ │
26+ U_activation, U_tiebreak ───────────────────────────────────────────────────────────────────────────────┘
27+ Z ──────────────────────────────────────────────────────────────────────────────────────────────────────┘ (direct: fewer heuristic agents, halts, limits)
28+```
29+
30+Z → Y is identified because Z is set by the design file and the only random input that depends on Z (`llm_assign`) is independent of every other stream by construction (`SeedSequence.spawn`). Fundamentals, news, endowments, latencies, information draws and policy assignment are *identical* across cells sharing a seed; the treatment replaces the policy in a stratified subset of slots and nothing else. Slot replacement is stratified by classical policy type (proportional quotas, market makers never replaced).
31+
32+Two leaks of common random numbers exist in the current code and must be closed before the pilot (gate 4 in `research_plan.md`): (i) the per-tick shuffle of active classical agents consumes the `activation` stream, whose length depends on the number of classical agents, so activation draws differ across arms after tick 0; (ii) an LLM slot draws its informed status from `llm.informed_share` (30 %) instead of inheriting it from the replaced slot, so the treatment arm holds more private information than the control (in a mock run, 44 % vs 6 % informed). Neither leak biases the ATE (Z is still randomized), but (ii) changes the *estimand* to "LLM policy + extra information" and (i) reduces the precision gain from pairing.
33+
34+### 1.3 Confounders ruled out by design
35+
36+| Potential confounder | Why it cannot confound Z → Y |
37+|---|---|
38+| Fundamental path, shocks, news | Same `fundamentals`/`news` streams within a seed; shocks at fixed ticks in every cell |
39+| Endowments, wealth dispersion, latencies | Same `slots` stream; LLM slots keep the replaced slot's cash, shares and latency |
40+| Composition of classical types | `assign` stream fixed; replacement stratified by type |
41+| Private information | After the gate-4 fix, inherited from the replaced slot |
42+| Market-maker behaviour | Market makers never replaced (`llm_keep_market_makers`) |
43+| Code/version drift | `config_hash`, `git_revision`, `prompt_version`, price table recorded in every manifest |
44+
45+### 1.4 What is *not* identified
46+
47+- **Mediation.** M is generated inside the run jointly with Y by the same path of U and ε. Sequential ignorability (M ⫫ Y(z, m) | Z, covariates) does not hold: e.g. a volatile fundamental path raises both ρ̄ and `msd_log`. We therefore report mediation only as a *descriptive decomposition* (§4.6) and treat each M as an **outcome** of Z (that is H2 and is identified).
48+- **Effects of the model's reasoning per se.** Z bundles the model, the prompt, the decision frequency and the invalid-decision rule. The estimand is intention-to-treat: "deploy this agent as configured".
49+
50+## 2. Estimands
51+
52+Unit = run (seed s × cell z). Y_s(z) is the potential outcome of seed s under cell z.
53+
54+| Label | Estimand | Contrast |
55+|---|---|---|
56+| E1 | ATE of cell z vs control: τ_z = E_s[Y_s(z) − Y_s(0)] | Each treated cell vs `share_0` of the same seed |
57+| E2 | Dose–response: τ(φ) for φ ∈ {0.01, 0.05, 0.10, 0.25, 0.50, 0.75, 1.00} with gpt-5.4-nano, single prompt; linear slope ∂τ/∂φ and ordered-trend test | Levels vs 0 and across levels |
58+| E3 | Diversity at equal share φ: (a) τ_div = E[Y(diverse, φ) − Y(single, φ)]; (b) τ_mix = E[Y(mix, φ) − ½(Y(nano, φ) + Y(mini, φ))] | (a) prompt diversity holding the model fixed; (b) model diversity net of composition (requires the mini-only cell) |
59+| E4 | Mechanism arms at φ = 0.25: memory 5 vs 0; channel on vs off; leverage high vs base; breaker vs none; position limit vs none | Each vs `share_25` |
60+| E5 | φ × leverage interaction: [Y(φ, lev_hi) − Y(0, lev_hi)] − [Y(φ, lev_base) − Y(0, lev_base)] | Difference of paired differences |
61+| E6 | Local projection of the mid–value gap around the t = 300 fundamental jump: β_h = E[g_s(z, h) − g_s(0, h)], g = log P − log V, h ∈ [−20, 100]; placebo at t = 450 | Treated vs control path |
62+| E7 | Realized-dose-scaled effect: τ_z / E[`llm_share_orders`] (descriptive, not causal per unit of order flow) | Reported alongside E1 |
63+
64+Primary confirmatory contrast: **E1 at φ = 0.25** (gpt-5.4-nano, single prompt, confirmatory fundamentals); secondary confirmatory: E1 at φ = 0.50 and E3(a). Everything else is exploratory unless the tiered budget allows the pre-specified seed count (`protocol.md` §5).
65+
66+## 3. Outcomes
67+
68+All variables are produced by `metrics/outcomes.py` on ticks t ≥ `warmup` = 100 (baseline.yaml), one row per run in `outcomes.parquet`. Multi-asset variants carry the suffix `_a1` for asset 1.
69+
70+### 3.1 Primary families and directional hypotheses
71+
72+| Family | Variable | Definition (short) | Direction under H | Hypothesis |
73+|---|---|---|---|---|
74+| Efficiency | `msd_log` **(lead H1, H4)** | mean (log P − log V)² | ↑ in φ; ↓ with diversity | H1, H4 |
75+| | `mad_pct` | mean 100·|log P − log V| | ↑ in φ | H1 |
76+| | `time_to_absorb` | first h after t = 300 with |P/V − 1| < 1 % for 5 ticks (censored at 100) | ↑ in φ; ↑ with safeguards | H1, H6 |
77+| | `response_ratio` | (P_{t+20} − P_{t−1}) / (V_{t+20} − V_{t−1}) | |RR − 1| ↑ in φ (two-sided on RR) | H1 |
78+| | `vr5` | VR(5) of log mid | |VR − 1| ↑ in φ (two-sided on VR) | H1 |
79+| Liquidity | `spread_bp` **(lead H1b)** | mean quoted spread / mid, bp | ↑ in φ | H1b |
80+| | `eff_spread_bp` | 2·|trade − pre-trade mid| / mid, bp | ↑ in φ | H1b |
81+| | `depth5` | mean lots within 5 levels, both sides | ↓ in φ | H1b |
82+| | `lambda_bp_100` | OLS of Δlog mid (bp) on signed volume / 100 | two-sided (theory: λ = Cov/Var falls as flow gets noisier) | H1b |
83+| | `resiliency` | half-life (ticks) of log-spread deviations | ↑ in φ | H1b |
84+| Instability | `n_crashes` **(lead H3)** | drops ≥ 10 % from the trailing 20-tick max | ↑ in φ × leverage | H3 |
85+| | `max_dd` **(lead H6)** | max drawdown of mid | ↑ in φ; ↓ with safeguards | H3, H6 |
86+| | `n_bubbles`, `bubble_amp` | episodes with |P/V − 1| > 5 % for ≥ 10 ticks; max amplitude | ↑ in φ × leverage | H3 |
87+| | `acf_absr_1_10` | Σ ACF(|r|) lags 1–10 | ↑ in φ | H3 |
88+| | `kurtosis` | excess kurtosis of tick returns | ↑ in φ | H3 |
89+| Coordination | `llm_pairwise_corr` **(lead H2)** | mean pairwise corr. of agents' net order sign per 20-tick window | ↑ in φ; ↓ with diversity | H2, H4 |
90+| | `llm_synchrony` **(lead H5)** | mean |mean sign| per decision round | ↑ with channel | H2, H5 |
91+| Contagion (K = 2) | `ret_corr`, `excess_comovement` **(lead)**, `spillover_dy` | return correlation; minus fundamental correlation; Diebold–Yilmaz total spillover (VAR(2), h = 10) | ↑ in φ | H2/H3 extension |
92+
93+Thresholds are fixed: bubble 5 % for ≥ 10 ticks; crash 10 % within 20 ticks. **Sensitivity grid** (reported for every instability result, no re-selection): bubble ∈ {3, 5, 8} %, crash ∈ {7, 10, 15} %. In the classical control (10 seeds) `n_crashes` = 0 at 10 %: a floor effect is expected and the 7 % variant is the pre-specified fallback for H3 (§`research_plan.md` §4).
94+
95+Transformations fixed in advance: `msd_log` is analysed on log10 scale for t-based inference (strongly right-skewed: control mean 0.0020, SD 0.0017); `time_to_absorb` is censored at 100 and analysed with rank-based inference; counts (`n_bubbles`, `n_crashes`, `n_halts`) by mean difference with randomization inference.
96+
97+### 3.2 Secondary outcomes
98+
99+`vol_bp`, `bias_pct`, `vr10`, `acf_r_1`, `bubble_dur`, `volume`, `turnover`, `cancel_ratio`, `fill_ratio`, `reject_ratio`, `n_halts`, `share_ticks_halted`, `pnl_<policy>` (incl. `pnl_llm`), `mm_pnl`, `mm_inventory_abs`, `gini_final`, `n_bankrupt`, `leadlag_01`, `fund_corr`, and the LLM diagnostics `llm_valid_rate`, `llm_hold_rate`, `llm_market_share`, `llm_buy_share`, `llm_usd`, `llm_tokens`, `llm_latency_s`, `llm_cached_hits`, plus the manifest fields `llm_share_orders`, `llm_share_volume`, `n_llm_decisions`.
100+
101+### 3.3 Known measurement caveats
102+
103+- `llm_synchrony` groups decisions by tick; with staggered offsets only agents sharing an offset are in the same round (≈ n_llm/20 per tick), so at φ ≤ 0.05 it rests on very few rounds. `llm_pairwise_corr` (20-tick windows) is the preferred coordination measure at low φ.
104+- Both coordination measures exist only for LLM agents, so they are undefined in the control; H2 is tested as a dose–response among φ > 0 plus a comparison with a classical analogue (§3.4).
105+- In multi-asset runs the prompt covers asset 0 only (`user_message(obs, 0)`): LLM agents never trade asset 1, so contagion is measured from the LLM-traded asset to a classical-only asset.
106+
107+### 3.4 Analysis-stage additions (to be coded in `analysis/`, not in `outcomes.py`)
108+
109+Classical analogue of ρ̄: the same `pairwise_sign_corr` applied to classical agents' aggressor-signed trades from `trades.parquet`, by policy type. Local-projection paths (E6) from `ticks.parquet`.
110+
111+## 4. Estimators
112+
113+### 4.1 Paired differences (primary)
114+
115+For each cell z and seed s: Δ_s = Y_s(z) − Y_s(0). Report mean Δ̄, SD, paired-t 95 % CI (df = n − 1) and the effect size d_z = Δ̄ / SD(Δ). Also Glass's Δ = Δ̄ / SD(Y(0)) using 30 free control seeds, for comparability across cells.
116+
117+### 4.2 Randomization inference (primary p-value)
118+
119+Under the sharp null Y_s(z) = Y_s(0), the sign of each Δ_s is exchangeable within its pair. P-value = share of the 2ⁿ sign-flip assignments with |mean Δ| ≥ |observed| (exact enumeration for n ≤ 14; 10⁵ Monte Carlo draws otherwise). This is distribution-free and valid for counts, censored and skewed outcomes at small n. The paired t (§4.1) and Wilcoxon signed-rank are reported as secondary.
120+
121+### 4.3 OLS with seed fixed effects (dose–response and multi-cell arms)
122+
123+Y_{s,z} = α_s + Σ_k τ_k 1[z = k] + ε_{s,z}. With ≤ 12 clusters, cluster-robust SE by seed are unreliable; inference uses the wild cluster bootstrap (Rademacher weights, 9,999 draws, cluster = seed) and, for ordered φ, **Page's L trend test** for randomized blocks (block = seed). The linear slope ∂τ/∂φ is reported with a bootstrap CI; the dose–response figure shows cell means with paired CIs.
124+
125+### 4.4 Diversity contrasts
126+
127+E3(a): paired difference diverse − single within seed. E3(b): τ_mix,s = Y_s(mix) − ½[Y_s(nano) + Y_s(mini)], then §4.1–4.2.
128+
129+### 4.5 Multiplicity, CIs, effect sizes
130+
131+Holm–Bonferroni within each hypothesis family of §3.1 at family-wise α = 0.05 (the lead outcome is reported adjusted and unadjusted). Two-sided tests; the sign of the estimate is compared with the predicted direction. 95 % CIs everywhere; no result is reported as a bare p-value. Secondary outcomes are unadjusted and labelled exploratory.
132+
133+### 4.6 Mediation (descriptive only)
134+
135+Within treated cells, regress Y on M with seed FE and cell dummies; report the coefficient and the share of τ_z "associated" with M (product-of-coefficients with the Z → M regression). Stated assumption: this is a decomposition under *no unmeasured mediator–outcome confounding*, which §1.4 argues is false; the numbers describe co-movement, not causal paths.
136+
137+### 4.7 Local projections (E6)
138+
139+For h ∈ [−20, 100]: g_s(z, h) = log P_{300+h} − log V_{300+h}. β_h = mean over seeds of g_s(z, h) − g_s(0, h), with pointwise paired-t bands and a sup-t band from the bootstrap. The same at the placebo tick 450 must show β_h ≈ 0 (falsification). Summary scalars: `time_to_absorb`, `response_ratio`.
140+
141+## 5. Power and seed counts
142+
143+Paired design, standardized paired effect d = Δ̄ / SD(Δ), two-sided α = 0.05, power 0.80:
144+
145+n ≈ (z_{0.975} + z_{0.80})² / d² = (1.960 + 0.842)² / d² = 7.85 / d²
146+
147+| d | n (pairs) |
148+|---|---|
149+| 0.5 | 32 |
150+| 0.8 | 13 |
151+| 1.0 | 8 |
152+| MDE at n = 10 | 0.89 |
153+| MDE at n = 8 | 0.99 |
154+
155+σ_d (the SD of paired differences) is **TBD from the pilot** (3 pairs per share; a poor estimate — the 30-seed classical control SD is used as a floor: e.g. SD(`msd_log`) = 0.0017, SD(`spread_bp`) = 6.4, SD(`max_dd`) = 0.042 in the default calibration cell, n = 10). Rule for the confirmatory seed count of the primary contrast: n_conf = max(8, ⌈7.85 σ̂_d² / Δ_min²⌉) with Δ_min = 0.5 σ̂_d (i.e. 32), **capped by the tier budget** (`protocol.md` §5: 10 seeds at 25 % and 50 % ≈ 10 USD). The achieved MDE is reported in the paper; detecting d = 0.5 on the primary contrast would require ≈ 32 USD for that contrast alone and is only done if extra budget is approved before unblinding. Classical-only cells cost nothing and use 30 seeds.
156+
157+## 6. Stopping rules, freeze, negative results
158+
159+### 6.1 Stopping rules
160+
161+- Pilot stops at 5 USD (hard `usd_cap`), or if provider failure rate > 10 %, or validity < 80 %; the prompt is then revised once (v2), re-dry-run with the mock, re-piloted on one seed.
162+- Confirmatory: tiers run in the fixed order of `protocol.md` §5; no interim efficacy looks; spend reconciled after each tier against the plan; a tier is complete only when every cell has the same seeds (unbalanced seeds are dropped from all cells of the tier). The 40 USD cap is enforced by per-run `usd_cap` and by the running total.
163+
164+### 6.2 Frozen after the pilot
165+
166+Prompt v1 text (`prompting.py` SHA) and schema; `configs/baseline.yaml` plus the confirmatory overrides (fundamentals with latent uncertainty, shock size) and their `config_hash`; cell list and seed lists (pilot seeds 1–3, confirmatory seeds from 101; pilot runs are *not* pooled into confirmatory estimates); outcome definitions (`outcomes.py` SHA), thresholds and sensitivity grid; estimators of §4; tier budgets; model snapshots and price table; the git revision. A freeze record is written to `results/manifests/freeze.json`.
167+
168+### 6.3 What counts as a negative result
169+
170+For a lead outcome with predicted sign: (i) *refuting*: CI of d excludes d ≥ 0.5 in the predicted direction; (ii) *no large effect*: CI contains 0 and excludes |d| ≥ MDE; (iii) *inconclusive*: CI contains both 0 and the MDE — reported as such, never as "no effect". Secondary outcomes cannot rescue a refuted lead.
171+
172+## 7. Threats to validity
173+
174+### 7.1 Internal (inside the simulator)
175+
176+| Threat | Consequence | Handling |
177+|---|---|---|
178+| Provider non-determinism (no seed; `temperature` unsupported on gpt-5) | Same seed ≠ same LLM decisions across repeated online runs | Every response cached with request id; replay from cache is exact; ε treated as part of the treatment variance; online-vs-replay flag `llm_cached_hits` reported |
179+| Cache replay across cells | Identical prompts (e.g. first decision at t = 50) hit the cache across cells — desirable CRN, but mixes online and replayed decisions | Report cache hit share per cell; cache key includes provider, model, prompt version and full prompt text |
180+| Invalid-decision handling (→ hold) | Validity differs by model; "hold" is a one-sided failure | ITT estimand; `llm_valid_rate` reported as outcome; cells with validity < 80 % flagged; no per-protocol rerun |
181+| Decision interval (20 ticks) vs classical activation (0.2–0.5 per tick) | Realized order share ≪ slot share (mock: 3.5 % of orders, 8 % of volume at φ = 0.25) | Report assigned and realized dose; E7; `decision_interval` = 10 sensitivity with the mock |
182+| Exact-revelation fundamentals in the baseline | V^pub ≡ V; private signals redundant; H1 unidentified | Confirmatory fundamentals: noisy dividends (`jump_prob` = 1, `jump_sigma` TBD ≈ 4 c → SD(V^pub/V − 1) ≈ 0.5 %) |
183+| Shock size (+8.4 % at 40 c) exceeds bubble threshold | Lagged adjustment counted as a bubble | Shock ≈ 10 c (+2.1 %) |
184+| Information-parity leak and activation-stream leak (§1.2) | Estimand drift; precision loss | Gate-4 fixes before the pilot |
185+| Budget halt mid-run (`halted_for_budget`) | Remaining decisions become hold | Per-run `usd_cap` set ≥ 2× expected spend; halted runs excluded and rerun |
186+| Model drift between pilot and confirmatory | Different behaviour over weeks | Confirmatory calls within a short window; model id and date in manifest |
187+| Frozen library (nearest-neighbour replay, k = 5) | Not an adaptive agent; distribution shift at N = 1,000 | Labelled provider `library`; used only for scale and cached-vs-online arms; hit distance diagnostics reported |
188+
189+### 7.2 External
190+
191+Simulated heuristic traders and prompted LLM agents are not institutions: no risk management, no regulation, no learning across runs (memory ≤ 5 own decisions), one asset at a time in the prompt, model time without calendar mapping, fees and latencies stylized. Effects are statements about *this* market mechanism and *these* model snapshots; magnitudes do not transfer, signs and dose–response shapes may.
added docs/literature_ledger.md +212 −0
@@ -0,0 +1,212 @@
1+# Literature ledger — WP 27 "The Invisible Hand of Machines"
2+
3+Claim-to-citation ledger. Every row was verified on 2026-10-27 against Crossref (`https://api.crossref.org/works/<DOI>`), the arXiv API / abstract page, or the publisher page named in the column. Bib keys match `paper/references.bib`. Items that could not be verified are listed at the end and are **not** in the bib file.
4+
5+
6+## (a) Agent-based computational economics and heterogeneous-agent models
7+
8+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
9+|---|---|---|---|---|---|---|---|
10+| 1 | A001 | Agent-based computational finance reproduces fat tails, volatility clustering and bubbles from interacting heterogeneous rules; survey we build on for the baseline rule-based population. | `lebaron2006` | 10.1016/S1574-0021(05)02024-1 | 2006 | Crossref (title/authors/venue matched) | 2026-10-27 |
11+| 2 | A002 | The Santa Fe artificial stock market shows that the speed of belief updating governs whether prices track fundamentals or display excess volatility and technical-trading regimes. | `lebaron1999` | 10.1016/S0165-1889(98)00081-5 | 1999 | Crossref (title/authors/venue matched) | 2026-10-27 |
12+| 3 | A003 | Interacting noise traders switching between optimistic/pessimistic and fundamentalist states generate scaling laws and clustered volatility endogenously. | `lux1999` | 10.1038/17290 | 1999 | Crossref (title/authors/venue matched) | 2026-10-27 |
13+| 4 | A004 | Volatility clustering arises from endogenous switching between chartist and fundamentalist strategies in a microsimulation. | `lux2000` | 10.1142/S0219024900000826 | 2000 | Crossref (title/authors/venue matched) | 2026-10-27 |
14+| 5 | A005 | Adaptive belief systems with discrete-choice switching between predictors produce complex (bifurcating) price dynamics as intensity of choice rises. | `brock1997` | 10.2307/2171879 | 1997 | Crossref (title/authors/venue matched) | 2026-10-27 |
15+| 6 | A006 | Fundamentalist/chartist heterogeneous-beliefs asset-pricing model: the share of trend-followers determines stability of the fundamental price (used to motivate our policy mix). | `brock1998` | 10.1016/S0165-1889(98)00011-6 | 1998 | Crossref (title/authors/venue matched) | 2026-10-27 |
16+| 7 | A007 | Simulated double-auction microstructure with fundamentalist, chartist and noise traders reproduces order-book stylized facts; template for our exchange design. | `chiarella2002` | 10.1088/1469-7688/2/5/303 | 2002 | Crossref (title/authors/venue matched) | 2026-10-27 |
17+| 8 | A008 | Heterogeneous trading rules in a limit-order book generate fat tails, volatility clustering and order-flow autocorrelation; chartist share drives excess volatility. | `chiarella2009` | 10.1016/j.jedc.2008.08.001 | 2009 | Crossref (title/authors/venue matched) | 2026-10-27 |
18+| 9 | A009 | Agent-based models are the appropriate tool for studying out-of-equilibrium market dynamics and policy counterfactuals that cannot be solved analytically. | `farmer2009` | 10.1038/460685a | 2009 | Crossref (title/authors/venue matched) | 2026-10-27 |
19+| 10 | A010 | Survey of heterogeneous-agent models: strategy switching and bounded rationality explain bubbles, crashes and excess volatility. | `hommes2006` | 10.1016/S1574-0021(05)02023-X | 2006 | Crossref (title/authors/venue matched) | 2026-10-27 |
20+| 11 | A011 | Complex-systems view of behavioral macro/finance: laboratory and ABM evidence that expectations heterogeneity drives aggregate instability; justifies treating LLM policies as another expectation type. | `hommes2021` | 10.1257/jel.20191434 | 2021 | Crossref (title/authors/venue matched) | 2026-10-27 |
21+| 12 | A012 | Recruitment/epidemic models of opinion switching generate persistent herding states without any informational reason. | `kirman1993` | 10.2307/2118498 | 1993 | Crossref (title/authors/venue matched) | 2026-10-27 |
22+| 13 | A013 | Herd behaviour among speculators generates bubbles and crashes in a mean-field model of contagious optimism. | `lux1995` | 10.2307/2235156 | 1995 | Crossref (title/authors/venue matched) | 2026-10-27 |
23+| 14 | A014 | Current state of agent-based modeling in economics and finance (JEL survey): validation, calibration and scale are the open frontiers our design addresses with matched seeds and power analysis. | `axtell2025` | 10.1257/jel.20221319 | 2025 | Crossref (title/authors/venue matched) | 2026-10-27 |
24+| 15 | A015 | Structural stochastic volatility models with herding (fundamentalist/chartist switching) can be estimated by matching moments of stylized facts; we use the same moment set for validation. | `franke2012` | 10.1016/j.jedc.2011.10.004 | 2012 | Crossref (title/authors/venue matched) | 2026-10-27 |
25+
26+## (b) Zero-intelligence, double-auction microstructure and information aggregation
27+
28+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
29+|---|---|---|---|---|---|---|---|
30+| 16 | B016 | Zero-intelligence traders constrained by budgets reach near-full allocative efficiency in double auctions: market structure, not trader rationality, drives efficiency (our ZI baseline). | `gode1993` | 10.1086/261868 | 1993 | Crossref (title/authors/venue matched) | 2026-10-27 |
31+| 17 | B017 | A zero-intelligence model of the continuous double auction predicts spread and price-impact relations from order-flow rates alone. | `farmer2005` | 10.1073/pnas.0409157102 | 2005 | Crossref (title/authors/venue matched) | 2026-10-27 |
32+| 18 | B018 | Statistical theory of the continuous double auction: order-flow parameters determine spread, depth and price diffusion in the ZI limit. | `smith2003` | 10.1088/1469-7688/3/6/307 | 2003 | Crossref (title/authors/venue matched) | 2026-10-27 |
33+| 19 | B019 | Empirical statistics of stock order books (limit-order placement distribution, book shape) used as reference facts for our simulated book. | `bouchaud2002` | 10.1088/1469-7688/2/4/301 | 2002 | Crossref (title/authors/venue matched) | 2026-10-27 |
34+| 20 | B020 | Stochastic order-book model with Markovian order flow: conditional probabilities of price moves computed from book state; benchmark for our book-state features. | `cont2010` | 10.1287/opre.1090.0780 | 2010 | Crossref (title/authors/venue matched) | 2026-10-27 |
35+| 21 | B021 | Order-flow imbalance (OFI) linearly predicts short-horizon price changes; we use OFI as a mechanism metric for synchronized LLM order flow. | `cont2014` | 10.1093/jjfinec/nbt003 | 2014 | Crossref (title/authors/venue matched) | 2026-10-27 |
36+| 22 | B022 | Reference treatment of order-book dynamics, price impact and the slow digestion of order flow; defines the impact and liquidity measures we report. | `bouchaud2018` | 10.1017/9781316659335 | 2018 | Crossref (title/authors/venue matched) | 2026-10-27 |
37+| 23 | B023 | Survey of limit-order-book models and empirical regularities used to parametrize the exchange. | `gould2013` | 10.1080/14697688.2013.803148 | 2013 | Crossref (title/authors/venue matched) | 2026-10-27 |
38+| 24 | B024 | Review of zero-intelligence models in economics and finance: ZI benchmarks isolate the contribution of market structure from agent intelligence. | `ladley2012` | 10.1017/s0269888912000173 | 2012 | Crossref (title/authors/venue matched) | 2026-10-27 |
39+| 25 | B025 | Continuous double auctions with induced values converge to competitive equilibrium with few traders; origin of the experimental double-auction paradigm. | `smith1962` | 10.1086/258609 | 1962 | Crossref (title/authors/venue matched) | 2026-10-27 |
40+| 26 | B026 | Experimental asset markets with known dividend streams produce bubbles and crashes; our dividend-paying asset and bubble metrics follow this design. | `smith1988` | 10.2307/1911361 | 1988 | Crossref (title/authors/venue matched) | 2026-10-27 |
41+| 27 | B027 | Insider trading with a risk-neutral market maker: price impact lambda = Cov(v,Y)/Var(Y) and the informativeness of prices is pinned down by informed-order variance relative to noise; our stylized model is a static Kyle variant. | `kyle1985` | 10.2307/1913210 | 1985 | Crossref (title/authors/venue matched) | 2026-10-27 |
42+| 28 | B028 | Bid-ask spreads arise from adverse selection when some traders are informed; informed share determines the spread. | `glosten1985` | 10.1016/0304-405X(85)90044-3 | 1985 | Crossref (title/authors/venue matched) | 2026-10-27 |
43+| 29 | B029 | Competitive noisy rational-expectations equilibrium: prices aggregate dispersed private signals imperfectly because of noise supply; benchmark for informativeness. | `hellwig1980` | 10.1016/0022-0531(80)90056-3 | 1980 | Crossref (title/authors/venue matched) | 2026-10-27 |
44+| 30 | B030 | Prices cannot be fully informationally efficient when information is costly; the equilibrium degree of informativeness is bounded. | `grossman1980` | https://www.jstor.org/stable/1805228 (AER 70(3):393-408) | 1980 | publisher/IDEAS record (ideas.repec.org/a/aea/aecrev/v70y1980i3p393-408.html); no Crossref DOI | 2026-10-27 |
45+| 31 | B031 | With correlated private signals among strategic informed traders, the correlation structure of signals (not only their precision) governs trading intensity and price informativeness. | `foster1996` | 10.1111/j.1540-6261.1996.tb04075.x | 1996 | Crossref (title/authors/venue matched) | 2026-10-27 |
46+| 32 | B032 | Imperfect competition among informed traders with correlated signals: more correlated information leads to faster revelation (rat race), with implications for how shared signals affect price discovery. | `back2000` | 10.1111/0022-1082.00282 | 2000 | Crossref (title/authors/venue matched) | 2026-10-27 |
47+| 33 | B033 | Public information can increase disagreement and trading volume through higher-order beliefs; shared public signals do not trivially homogenize behaviour. | `kondor2012` | 10.1093/restud/rdr051 | 2012 | Crossref (title/authors/venue matched) | 2026-10-27 |
48+| 34 | B034 | Information diversity (different traders informed about different components) is complementary; loss of diversity reduces price informativeness about some components. | `goldstein2015` | 10.1111/jofi.12226 | 2015 | Crossref (title/authors/venue matched) | 2026-10-27 |
49+| 35 | B035 | Abundant cheap data can lower price informativeness about fundamentals by crowding out costly deep information; parallel to shared-model crowding. | `dugast2018` | 10.1016/j.jfineco.2018.07.004 | 2018 | Crossref (title/authors/venue matched) | 2026-10-27 |
50+| 36 | B036 | Realism metrics (stylized facts) for evaluating limit-order-book simulations; adopted as validation checklist. | `vyetrenko2020` | 10.1145/3383455.3422561 | 2020 | Crossref (title/authors/venue matched) | 2026-10-27 |
51+| 37 | B037 | ABIDES: discrete-event multi-agent market simulator with latency and a NASDAQ-like matching engine; our exchange follows the same event-driven design. | `byrd2020` | 10.1145/3384441.3395986 | 2020 | Crossref (title/authors/venue matched) | 2026-10-27 |
52+
53+## (c) Price discovery and information-efficiency measurement
54+
55+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
56+|---|---|---|---|---|---|---|---|
57+| 38 | C038 | Information content of trades measured by a VAR of quote revisions and trades (permanent price impact); we use the Hasbrouck impulse-response measure. | `hasbrouck1991` | 10.1111/j.1540-6261.1991.tb03749.x | 1991 | Crossref (title/authors/venue matched) | 2026-10-27 |
58+| 39 | C039 | Decomposition of transaction price into an efficient (random-walk) component and a pricing error; the standard deviation of the pricing error is our market-quality metric. | `hasbrouck1993` | 10.1093/rfs/6.1.191 | 1993 | Crossref (title/authors/venue matched) | 2026-10-27 |
59+| 40 | C040 | Information shares attribute price discovery to venues/agents via the common efficient price; adapted to attribute discovery to LLM vs rule-based order flow. | `hasbrouck1995` | 10.1111/j.1540-6261.1995.tb04054.x | 1995 | Crossref (title/authors/venue matched) | 2026-10-27 |
60+| 41 | C041 | Variance-ratio test rejects random walk under positive autocorrelation; we use variance ratios as an efficiency statistic. | `lo1988` | 10.1093/rfs/1.1.41 | 1988 | Crossref (title/authors/venue matched) | 2026-10-27 |
61+| 42 | C042 | Effective bid-ask spread can be inferred from the serial covariance of price changes. | `roll1984` | 10.1111/j.1540-6261.1984.tb03897.x | 1984 | Crossref (title/authors/venue matched) | 2026-10-27 |
62+| 43 | C043 | Illiquidity measured by absolute return per unit of volume; used as our price-impact proxy. | `amihud2002` | 10.1016/S1386-4181(01)00024-6 | 2002 | Crossref (title/authors/venue matched) | 2026-10-27 |
63+| 44 | C044 | Price informativeness is measured by how well prices predict future fundamentals (forecasting-regression R2); we use this to define informativeness about the latent dividend process. | `bai2016` | 10.1016/j.jfineco.2016.08.005 | 2016 | Crossref (title/authors/venue matched) | 2026-10-27 |
64+| 45 | C045 | Price informativeness matters for real decisions (feedback effects), motivating informativeness as a welfare-relevant outcome. | `bond2012` | 10.1146/annurev-financial-110311-101826 | 2012 | Crossref (title/authors/venue matched) | 2026-10-27 |
65+| 46 | C046 | Transitory price pressures from inventory imbalances are distinguished from permanent information effects; justifies separating pricing error from efficient-price variance. | `hendershott2014` | 10.1016/j.jfineco.2014.08.001 | 2014 | Crossref (title/authors/venue matched) | 2026-10-27 |
66+
67+## (d) Liquidity spirals, fire sales and margin
68+
69+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
70+|---|---|---|---|---|---|---|---|
71+| 47 | D047 | Funding liquidity and market liquidity reinforce each other through margins, producing liquidity spirals; our margin-call mechanism follows this logic. | `brunnermeier2009` | 10.1093/rfs/hhn098 | 2009 | Crossref (title/authors/venue matched) | 2026-10-27 |
72+| 48 | D048 | Liquidation values fall when potential buyers are themselves constrained; fire-sale discounts emerge from industry-wide distress. | `shleifer1992` | 10.1111/j.1540-6261.1992.tb04661.x | 1992 | Crossref (title/authors/venue matched) | 2026-10-27 |
73+| 49 | D049 | Limits of arbitrage: constrained arbitrageurs can amplify rather than correct mispricing when losses trigger withdrawals. | `shleifer1997` | 10.1111/j.1540-6261.1997.tb03807.x | 1997 | Crossref (title/authors/venue matched) | 2026-10-27 |
74+| 50 | D050 | Fire sales in finance and macroeconomics: forced sales by constrained agents push prices below fundamentals and propagate losses. | `shleifer2011` | 10.1257/jep.25.1.29 | 2011 | Crossref (title/authors/venue matched) | 2026-10-27 |
75+| 51 | D051 | The leverage cycle: endogenous margins amplify booms and crashes; leverage caps are a policy lever. | `geanakoplos2010` | 10.1086/648285 | 2010 | Crossref (title/authors/venue matched) | 2026-10-27 |
76+| 52 | D052 | Procyclical leverage of intermediaries: balance-sheet adjustment amplifies price moves. | `adrian2010` | 10.1016/j.jfi.2008.12.002 | 2010 | Crossref (title/authors/venue matched) | 2026-10-27 |
77+| 53 | D053 | Financially constrained arbitrageurs may under-provide liquidity; welfare effects of constraints. | `gromb2002` | 10.1016/S0304-405X(02)00228-3 | 2002 | Crossref (title/authors/venue matched) | 2026-10-27 |
78+| 54 | D054 | Leverage in an agent-based model generates fat tails and clustered volatility via synchronized margin-driven selling; closest ABM antecedent of our leverage treatment. | `thurner2012` | 10.1080/14697688.2012.674301 | 2012 | Crossref (title/authors/venue matched) | 2026-10-27 |
79+| 55 | D055 | Contagion as a wealth effect: convergence traders' losses in one asset propagate to others through reduced risk-bearing capacity. | `kyle2001` | 10.1111/0022-1082.00373 | 2001 | Crossref (title/authors/venue matched) | 2026-10-27 |
80+| 56 | D056 | Distressed selling by funds induces endogenous correlation between assets; multi-asset spillover mechanism in our K>1 runs. | `cont2013` | 10.1111/j.1467-9965.2011.00510.x | 2013 | Crossref (title/authors/venue matched) | 2026-10-27 |
81+| 57 | D057 | Fire-sale spillovers across institutions scale with common exposures and leverage; aggregate vulnerability index. | `greenwood2015` | 10.1016/j.jfineco.2014.11.006 | 2015 | Crossref (title/authors/venue matched) | 2026-10-27 |
82+
83+## (e) Herding, correlated trading, algorithmic trading and flash crashes
84+
85+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
86+|---|---|---|---|---|---|---|---|
87+| 58 | E058 | Herding of institutional investors is measurable by the buy-side imbalance statistic (LSV measure) which we compute for LLM cohorts. | `lakonishok1992` | 10.1016/0304-405X(92)90023-Q | 1992 | Crossref (title/authors/venue matched) | 2026-10-27 |
88+| 59 | E059 | During the Flash Crash, HFTs did not trigger the crash but their synchronized inventory management amplified it; crash-amplification channel. | `kirilenko2017` | 10.1111/jofi.12498 | 2017 | Crossref (title/authors/venue matched) | 2026-10-27 |
89+| 60 | E060 | High-frequency market makers behave as new market makers with tight inventory control; informs our market-maker policy. | `menkveld2013` | 10.1016/j.finmar.2013.06.006 | 2013 | Crossref (title/authors/venue matched) | 2026-10-27 |
90+| 61 | E061 | Algorithmic trading improves liquidity on average (causal evidence from NYSE autoquote). | `hendershott2011` | 10.1111/j.1540-6261.2010.01624.x | 2011 | Crossref (title/authors/venue matched) | 2026-10-27 |
91+| 62 | E062 | Continuous-time trading creates a latency arms race with correlated sniping; frequent batch auctions as alternative design (motivates our latency and batching treatments). | `budish2015` | 10.1093/qje/qjv027 | 2015 | Crossref (title/authors/venue matched) | 2026-10-27 |
92+| 63 | E063 | Informational cascades: rational agents ignore private signals after observing predecessors; cascade probability rises with signal correlation. | `bikhchandani1992` | 10.1086/261849 | 1992 | Crossref (title/authors/venue matched) | 2026-10-27 |
93+| 64 | E064 | Simple model of herd behavior: sequential decision-makers follow others, destroying information aggregation. | `banerjee1992` | 10.2307/2118364 | 1992 | Crossref (title/authors/venue matched) | 2026-10-27 |
94+| 65 | E065 | Reputational herding: managers mimic others' decisions; shared evaluation produces correlated choices. | `scharfstein1990` | https://www.jstor.org/stable/2006678 (AER 80(3):465-479) | 1990 | IDEAS record (ideas.repec.org/a/aea/aecrev/v80y1990i3p465-79.html); no Crossref DOI | 2026-10-27 |
95+| 66 | E066 | Short-horizon speculators herd on the same information source, which can be unrelated to fundamentals. | `froot1992` | 10.1111/j.1540-6261.1992.tb04665.x | 1992 | Crossref (title/authors/venue matched) | 2026-10-27 |
96+| 67 | E067 | Algorithmic trading in FX: algorithmic strategies are more correlated with each other than human trades, yet improve price efficiency. | `chaboud2014` | 10.1111/jofi.12186 | 2014 | Crossref (title/authors/venue matched) | 2026-10-27 |
97+| 68 | E068 | HFTs trade in the direction of permanent price changes and against transitory pricing errors, improving price discovery. | `brogaard2014` | 10.1093/rfs/hhu032 | 2014 | Crossref (title/authors/venue matched) | 2026-10-27 |
98+| 69 | E069 | Review of the economics of HFT: speed, adverse selection, and liquidity consequences. | `menkveld2016` | 10.1146/annurev-financial-121415-033010 | 2016 | Crossref (title/authors/venue matched) | 2026-10-27 |
99+| 70 | E070 | The August 2007 quant unwind: common strategies produced simultaneous deleveraging, evidence of crowding among algorithmic funds. | `khandani2011` | 10.1016/j.finmar.2010.07.005 | 2011 | Crossref (title/authors/venue matched) | 2026-10-27 |
100+| 71 | E071 | Order-flow toxicity (VPIN) rose before the Flash Crash; liquidity provision withdrew when flow became toxic. | `easley2011` | 10.3905/jpm.2011.37.2.118 | 2011 | Crossref (title/authors/venue matched) | 2026-10-27 |
101+| 72 | E072 | News trading and speed: fast traders react to common signals, affecting price informativeness and liquidity. | `foucault2016` | 10.1111/jofi.12302 | 2016 | Crossref (title/authors/venue matched) | 2026-10-27 |
102+| 73 | E073 | Ultrafast extreme events (mini flash crashes) proliferate in a machine ecology beyond human response time. | `johnson2013` | 10.1038/srep02627 | 2013 | Crossref (title/authors/venue matched) | 2026-10-27 |
103+
104+## (f) Stylized facts
105+
106+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
107+|---|---|---|---|---|---|---|---|
108+| 74 | F074 | Catalogue of stylized facts of asset returns (fat tails, absence of linear autocorrelation, volatility clustering, leverage effect) used as validation targets. | `cont2001` | 10.1080/713665670 | 2001 | Crossref (title/authors/venue matched) | 2026-10-27 |
109+| 75 | F075 | Speculative price changes have heavy (stable-Paretian) tails. | `mandelbrot1963` | 10.1086/294632 | 1963 | Crossref (title/authors/venue matched) | 2026-10-27 |
110+| 76 | F076 | ARCH: conditional heteroskedasticity captures volatility clustering. | `engle1982` | 10.2307/1912773 | 1982 | Crossref (title/authors/venue matched) | 2026-10-27 |
111+| 77 | F077 | GARCH generalization used to estimate volatility persistence in simulated returns. | `bollerslev1986` | 10.1016/0304-4076(86)90063-1 | 1986 | Crossref (title/authors/venue matched) | 2026-10-27 |
112+| 78 | F078 | Efficient markets hypothesis: definition of weak/semi-strong/strong form efficiency that our efficiency metrics operationalize. | `fama1970` | 10.2307/2325486 | 1970 | Crossref (title/authors/venue matched) | 2026-10-27 |
113+| 79 | F079 | Absolute returns display long memory; power-law decay of the autocorrelation of absolute returns. | `ding1993` | 10.1016/0927-5398(93)90006-D | 1993 | Crossref (title/authors/venue matched) | 2026-10-27 |
114+| 80 | F080 | Order-flow signs display long memory while prices remain nearly efficient: liquidity adapts to predictable flow. | `lillo2004` | 10.2202/1558-3708.1226 | 2004 | Crossref (title/authors/venue matched) | 2026-10-27 |
115+
116+## (g) Contagion and spillovers
117+
118+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
119+|---|---|---|---|---|---|---|---|
120+| 81 | G081 | Spillover index from forecast-error variance decompositions measures return and volatility spillovers. | `diebold2009` | 10.1111/j.1468-0297.2008.02208.x | 2009 | Crossref (title/authors/venue matched) | 2026-10-27 |
121+| 82 | G082 | Generalized (order-invariant) directional spillover measures. | `diebold2012` | 10.1016/j.ijforecast.2011.02.006 | 2012 | Crossref (title/authors/venue matched) | 2026-10-27 |
122+| 83 | G083 | Variance decompositions define connectedness networks among financial entities; we apply them across assets and agent cohorts. | `diebold2014` | 10.1016/j.jeconom.2014.04.012 | 2014 | Crossref (title/authors/venue matched) | 2026-10-27 |
123+| 84 | G084 | Correlation-based contagion tests must adjust for heteroskedasticity; otherwise interdependence is mistaken for contagion. | `forbes2002` | 10.1111/0022-1082.00494 | 2002 | Crossref (title/authors/venue matched) | 2026-10-27 |
124+| 85 | G085 | Rational-expectations contagion: cross-market rebalancing by informed investors transmits shocks between assets with unrelated fundamentals. | `kodres2002` | 10.1111/1540-6261.00441 | 2002 | Crossref (title/authors/venue matched) | 2026-10-27 |
125+| 86 | G086 | Dense financial networks are robust to small shocks but fragile to large ones (phase transition). | `acemoglu2015` | 10.1257/aer.20130456 | 2015 | Crossref (title/authors/venue matched) | 2026-10-27 |
126+| 87 | G087 | Information heterogeneity among strategic speculators generates financial contagion; homogeneity reduces it. | `pasquariello2007` | 10.1093/rfs/hhl010 | 2007 | Crossref (title/authors/venue matched) | 2026-10-27 |
127+
128+## (h) LLM agents in markets and economics; algorithmic collusion and monoculture
129+
130+| # | claim id | exact claim we will make in the paper | citation key | DOI or URL | year | how verified | verification date |
131+|---|---|---|---|---|---|---|---|
132+| 88 | H088 | LLMs can be used as simulated economic agents (homo silicus) to replicate classic behavioural experiments at low cost. | `horton2023` | 10.3386/w31122 | 2023 | Crossref (title/authors/venue matched) | 2026-10-27 |
133+| 89 | H089 | EconAgent: LLM-empowered agents with perception, memory and reflection produce realistic macroeconomic dynamics in a simulation. | `li2024econagent` | 10.18653/v1/2024.acl-long.829 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
134+| 90 | H090 | ChatGPT-derived sentiment scores on headlines predict next-day stock returns out of sample. | `lopezlira2023chatgpt` | arXiv:2304.07619 (also SSRN 10.2139/ssrn.4412788) | 2023 | arXiv API + Crossref (SSRN) | 2026-10-27 |
135+| 91 | H091 | LLM economic forecasts are contaminated by memorization of historical data; backtests of LLM agents on real histories are unreliable. | `lopezlira2025memorization` | arXiv:2504.14765 | 2025 | arXiv API | 2026-10-27 |
136+| 92 | H092 | GPT choices in budgetary decision tasks score higher on rationality (GARP consistency) than humans and are largely stable across demographic prompts. | `chen2023pnas` | 10.1073/pnas.2316205120 | 2023 | Crossref (title/authors/venue matched) | 2026-10-27 |
137+| 93 | H093 | FinMem: single LLM trading agent with layered memory and character design evaluated by backtesting on real stock histories. | `yu2023finmem` | arXiv:2311.13743 | 2023 | arXiv API | 2026-10-27 |
138+| 94 | H094 | TradingGPT: multi-agent LLM trading system with layered memory and distinct characters, evaluated on historical returns. | `li2023tradinggpt` | arXiv:2309.03736 | 2023 | arXiv API | 2026-10-27 |
139+| 95 | H095 | FinAgent: multimodal foundation agent for financial trading, evaluated against trading benchmarks on historical data. | `zhang2024finagent` | 10.1145/3637528.3671801 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
140+| 96 | H096 | StockAgent: LLM-driven multi-agent stock-trading simulation with bulletin-board communication; GPT- vs Gemini-driven agents differ systematically in trading behaviour. | `zhang2024stockagent` | arXiv:2407.18957 | 2024 | arXiv API | 2026-10-27 |
141+| 97 | H097 | ASFM: agent-based simulated financial market with LLM traders and a real order-matching system; reactions to rate and inflation shocks match economic priors. | `gao2024asfm` | arXiv:2406.19966 | 2024 | arXiv API | 2026-10-27 |
142+| 98 | H098 | TwinMarket: scalable LLM-agent stock-market simulation with social-media interaction reproduces bubbles and crashes; reports that agents of one model family are more homogeneous. | `yang2025twinmarket` | arXiv:2502.01506 (NeurIPS 2025 proceedings page) | 2025 | arXiv API + proceedings.neurips.cc listing | 2026-10-27 |
143+| 99 | H099 | LLM agents in a persistent order-book simulation adhere to assigned strategies, generate price discovery and bubbles, and can be analysed with partial-dependence-style perturbations. | `lopezlira2025trade` | arXiv:2504.10789 | 2025 | arXiv API | 2026-10-27 |
144+| 100 | H100 | In Smith-Suchanek-Williams-type experimental markets, LLM agents price near fundamentals and form only muted bubbles unlike humans; LLM strategy text has lower variance. | `henning2025` | arXiv:2502.15800 | 2025 | arXiv API | 2026-10-27 |
145+| 101 | H101 | LLM agents in a simulated call auction exhibit disposition effects and extrapolative beliefs; prompt interventions causally change bubble size. | `ouyang2026` | arXiv:2604.18373 | 2026 | arXiv abstract page | 2026-10-27 |
146+| 102 | H102 | Across 15 LLMs, behaviour ranges from fundamental pricing to human-like bubbles; mixed populations are unstable and advanced models adapt to exploit weaker ones, raising volatility. | `saxena2026` | arXiv:2604.18602 | 2026 | arXiv abstract page | 2026-10-27 |
147+| 103 | H103 | Replications of Smith (1962) double auctions with LLM agents converge more slowly or not at all to competitive equilibrium, with model-family differences. | `struski2026` | arXiv:2609.02580 | 2026 | arXiv abstract page | 2026-10-27 |
148+| 104 | H104 | Market institution (call market, CDA, posted offer, bargaining) changes efficiency of identical LLM agents from 56% to 89%. | `chupilkin2026` | arXiv:2608.04020 | 2026 | arXiv abstract page | 2026-10-27 |
149+| 105 | H105 | FCLAgent: LLM-decided buy/sell with rule-based order sizing reproduces path-dependent price patterns absent from conventional ABMs. | `hashimoto2025` | arXiv:2510.12189 | 2025 | arXiv abstract page | 2026-10-27 |
150+| 106 | H106 | StockSim: open order-level simulator with latency, slippage and price-time matching for evaluating multi-agent LLMs. | `papadakis2025` | arXiv:2507.09255 | 2025 | arXiv abstract page | 2026-10-27 |
151+| 107 | H107 | LLM agents' trading-style switching in a year-long stock simulation is only partially consistent with behavioural-finance theory. | `li2026switch` | arXiv:2602.07023 | 2026 | arXiv API | 2026-10-27 |
152+| 108 | H108 | Overlap in retrieved information among RAG-based LLM trading agents raises mean pairwise action correlation from 0.04 to 0.41 and increases position volatility without raising profits. | `song2026` | https://openreview.net/forum?id=8v4FskZp8I | 2026 | ICLR 2026 workshop page (iclr.cc/virtual/2026/10016534); no arXiv version found | 2026-10-27 |
153+| 109 | H109 | LLM agents in multi-agent systems conform to peers; the confidence gap and format of peer information control the strength of herding. | `cho2025` | arXiv:2505.21588 | 2025 | arXiv abstract page | 2026-10-27 |
154+| 110 | H110 | LLM sellers in continuous double auctions collude more when they can communicate; collusion propensity differs across models. | `agrawal2025` | arXiv:2507.01413 | 2025 | arXiv abstract page | 2026-10-27 |
155+| 111 | H111 | LLM-based pricing agents autonomously reach supracompetitive prices; small prompt wording changes alter the degree of collusion. | `fish2024` | arXiv:2404.00806 | 2024 | arXiv API | 2026-10-27 |
156+| 112 | H112 | Q-learning pricing algorithms learn to collude tacitly in repeated oligopoly without communication. | `calvano2020` | 10.1257/aer.20190623 | 2020 | Crossref (title/authors/venue matched) | 2026-10-27 |
157+| 113 | H113 | Q-learning under sequential pricing converges to supracompetitive (Edgeworth-cycle) outcomes. | `klein2021` | 10.1111/1756-2171.12383 | 2021 | Crossref (title/authors/venue matched) | 2026-10-27 |
158+| 114 | H114 | Empirical evidence of higher margins after adoption of algorithmic pricing in German retail gasoline. | `assad2024` | 10.1086/726906 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
159+| 115 | H115 | Asymmetric pricing-algorithm adoption softens competition through commitment and frequency effects. | `brown2023` | 10.1257/mic.20210158 | 2023 | Crossref (title/authors/venue matched) | 2026-10-27 |
160+| 116 | H116 | Reinforcement-learning informed speculators in a Kyle-type market sustain supracompetitive profits via price-trigger strategies or homogenized learning biases, lowering price informativeness and liquidity. | `dou2025` | 10.3386/w34054 | 2025 | Crossref (title/authors/venue matched) | 2026-10-27 |
161+| 117 | H117 | Q-learning market makers learn adverse-selection pricing but quote markups over competitive spreads, shrinking with the number of algorithms. | `colliard2026` | 10.1093/rfs/hhag010 | 2026 | Crossref (title/authors/venue matched) | 2026-10-27 |
162+| 118 | H118 | Algorithmic monoculture: when many decision-makers rely on the same algorithm, outcomes can be worse even if the algorithm is individually more accurate. | `kleinberg2021` | 10.1073/pnas.2018340118 | 2021 | Crossref (title/authors/venue matched) | 2026-10-27 |
163+| 119 | H119 | Shared foundation models produce outcome homogenization: the same individuals are systematically mis-served across deployers. | `bommasani2022` | arXiv:2211.13972 | 2022 | arXiv API | 2026-10-27 |
164+| 120 | H120 | Foundation models create homogenization risk: downstream systems inherit the same strengths and failure modes. | `bommasani2021` | arXiv:2108.07258 | 2021 | arXiv API | 2026-10-27 |
165+| 121 | H121 | Generative AI raises individual creativity but reduces collective diversity of outputs. | `doshi2024` | 10.1126/sciadv.adn5290 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
166+| 122 | H122 | Standard LLM outputs show diminished diversity of thought relative to human samples. | `park2024brm` | 10.3758/s13428-023-02307-x | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
167+| 123 | H123 | LLM assistance homogenizes human creative ideation across users. | `anderson2024` | 10.1145/3635636.3656204 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
168+| 124 | H124 | S3: social-network simulation with LLM-empowered agents reproduces emotion, attitude and information propagation. | `gao2023s3` | arXiv:2307.14984 | 2023 | arXiv API | 2026-10-27 |
169+| 125 | H125 | Generative agents with memory, reflection and planning produce believable emergent social behaviour. | `park2023` | 10.1145/3586183.3606763 | 2023 | Crossref (title/authors/venue matched) | 2026-10-27 |
170+| 126 | H126 | LLMs can simulate multiple humans and replicate classic human-subject studies (Turing experiments), with fidelity varying by model and task. | `aher2023` | https://proceedings.mlr.press/v202/aher23a.html | 2023 | publisher page (PMLR v202:337-371) + arXiv API 2208.10264 | 2026-10-27 |
171+| 127 | H127 | Conditioning LLMs on demographic backstories produces silicon samples that match human sub-populations' response distributions. | `argyle2023` | 10.1017/pan.2023.2 | 2023 | Crossref (title/authors/venue matched) | 2026-10-27 |
172+| 128 | H128 | Expectations elicited from an LLM reading WSJ articles track professional forecaster surveys and are extrapolative. | `bybee2023` | arXiv:2305.02823 | 2023 | arXiv API | 2026-10-27 |
173+| 129 | H129 | Automated social science: LLMs generate hypotheses and act as subjects in simulated experiments. | `manning2024` | arXiv:2404.11794 | 2024 | arXiv API | 2026-10-27 |
174+| 130 | H130 | ChatGPT choices in behavioural games fall within the human distribution and are more cooperative/altruistic than the median human. | `mei2024` | 10.1073/pnas.2313925121 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
175+| 131 | H131 | LLMs in repeated games cooperate well in self-interested games but coordinate poorly, revealing systematic behavioural signatures. | `akata2025` | 10.1038/s41562-025-02172-y | 2025 | Crossref (title/authors/venue matched) | 2026-10-27 |
176+| 132 | H132 | LLM service behaviour drifts over months (GPT-3.5/4 March vs June 2023), motivating frozen decision libraries for replicability. | `chen2024hdsr` | 10.1162/99608f92.5317da47 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
177+| 133 | H133 | Ensembles of twelve LLMs match human crowd forecasting accuracy (wisdom of the silicon crowd), illustrating diversity benefits across models. | `schoenegger2024` | 10.1126/sciadv.adp1528 | 2024 | Crossref (title/authors/venue matched) | 2026-10-27 |
178+| 134 | H134 | Multi-agent reinforcement-learning stock market reproduces stylized facts; RL-agent precursor of LLM-populated markets. | `lussange2021` | 10.1007/s10614-020-10038-w | 2021 | Crossref (title/authors/venue matched) | 2026-10-27 |
179+| 135 | H135 | Generative AI agents can be modelled as economic agents with new primitives (prompts, context); framework for LLM-agent economics. | `immorlica2024` | arXiv:2406.00477 | 2024 | arXiv API | 2026-10-27 |
180+| 136 | H136 | LLM social simulations are a promising method provided validity concerns (homogeneity, sycophancy, drift) are addressed. | `anthis2025` | arXiv:2504.02234 | 2025 | arXiv API | 2026-10-27 |
181+| 137 | H137 | Deep learning adoption increases uniformity and interconnectedness in finance; models built on the same data generate correlated predictions. | `gensler2020` | 10.2139/ssrn.3723132 | 2020 | Crossref (title/authors/venue matched) | 2026-10-27 |
182+| 138 | H138 | Widespread use of common AI models and data sources could increase correlations in trading, lending and pricing (FSB 2024). | `fsb2024` | https://www.fsb.org/uploads/P14112024.pdf | 2024 | publisher page (fsb.org, 14 Nov 2024) | 2026-10-27 |
183+| 139 | H139 | AI in risk management can create endogenous systemic risk through common models and procyclical responses. | `danielsson2022` | 10.1016/j.jbankfin.2021.106290 | 2022 | Crossref (title/authors/venue matched) | 2026-10-27 |
184+| 140 | H140 | AI amplifies existing vulnerabilities and makes crises faster and more severe in a game-theoretic model of AI-driven markets. | `danielsson2025` | 10.1016/j.jfs.2025.101453 | 2025 | Crossref (title/authors/venue matched) | 2026-10-27 |
185+| 141 | H141 | Intelligent financial system: AI transforms intermediation, asset management and payments, with monoculture and correlation among the financial-stability concerns. | `aldasoro2025` | 10.1016/j.jfs.2025.101472 | 2025 | Crossref (title/authors/venue matched) | 2026-10-27 |
186+| 142 | H142 | In LLM re-creations of herding experiments, AI agents rely more on private signals than humans, but can be prompted to herd when maximizing profit. | `hansen2025` | 10.17016/FEDS.2025.090 | 2025 | Crossref (title/authors/venue matched) | 2026-10-27 |
187+| 143 | H143 | Unified model in which AI adoption share and signal correlation raise systemic-risk coupling convexly; 13F evidence of tail-loss amplification. | `meng2026` | arXiv:2604.03272 | 2026 | arXiv abstract page | 2026-10-27 |
188+
189+## Summary counts
190+
191+- Verified entries in this ledger: **143**, one per bib key; `paper/references.bib` has 143 entries (every ledger row has a bib entry and vice versa; cross-checked with `comm` on 2026-10-27).
192+- Could not verify / replaced: see next section (none of these are in the bib).
193+
194+## Could not verify (NOT in references.bib)
195+
196+| item | what we looked for | why excluded |
197+|---|---|---|
198+| "ElliottAgents" / other 2025 LLM-trading benchmark names seen in secondary blogs | a primary arXiv or proceedings record | no primary record found; blog mentions only |
199+| Danielsson & Uthemann, "Artificial intelligence and financial crises" arXiv:2407.17048 | journal reference on the arXiv page | arXiv page shows no journal-ref; we cite the Journal of Financial Stability version `danielsson2025` (Crossref 10.1016/j.jfs.2025.101453) instead |
200+| Lopez-Lira & Tang (2023) journal version | a peer-reviewed journal DOI | still an arXiv/SSRN preprint as of v6 (Oct 2025); cited as `lopezlira2023chatgpt` (arXiv) |
201+| Dou, Goldstein & Ji — journal version | a journal DOI | only NBER w34054 / SSRN 4452704 exist; cited as `dou2025` (NBER, Crossref 10.3386/w34054) |
202+| Colliard, Foucault & Lovo — volume/pages | RFS volume, issue, pages | Crossref lists the RFS article (10.1093/rfs/hhag010) with no volume/pages yet; bib entry carries `note = {Forthcoming}` |
203+| Song (2026) — arXiv version | an arXiv ID for the ICLR 2026 workshop paper | none found; cited via the OpenReview/ICLR page |
204+| Byrd, Hybinette & Balch 2019 arXiv 1904.12066 | arXiv API record (rate-limited, HTTP 429) | abstract page fetched instead; we cite the SIGSIM-PADS 2020 version `byrd2020` (Crossref 10.1145/3384441.3395986) |
205+| Arthur, Holland, LeBaron, Palmer & Tayler (1997) Santa Fe chapter | a DOI for the book chapter in *The Economy as an Evolving Complex System II* | no Crossref record; we cite `lebaron1999` (JEDC) for the Santa Fe market instead |
206+| Hasbrouck (2007) *Empirical Market Microstructure* (book) | a DOI | book has no DOI; not needed since `hasbrouck1991`/`hasbrouck1993`/`hasbrouck1995` cover the measures |
207+| Vives (2008) *Information and Learning in Markets* | a DOI | book without Crossref DOI; `hellwig1980`, `grossman1980`, `kyle1985` suffice |
208+| Aher et al. 2023 ICML via ACM DL (10.5555/3618408.3618425) | Crossref record | 10.5555 is not registered at Crossref; cited via the PMLR page instead |
209+| Aldasoro et al. — guessed DOI 10.1016/j.jfs.2025.101456 | Crossref | resolved to a different paper; corrected to 10.1016/j.jfs.2025.101472 (now `aldasoro2025`) |
210+| Thurner, Farmer & Geanakoplos — guessed DOI 10.1080/14697688.2011.586336 | Crossref | not found; corrected to 10.1080/14697688.2012.674301 (now `thurner2012`) |
211+| Pasquariello — guessed DOI 10.1093/rfs/hhl046 | Crossref | not found; corrected to 10.1093/rfs/hhl010 (now `pasquariello2007`) |
212+| Chiarella–Iori–Perelló, Back–Cao–Willard, Foster–Viswanathan, Ladley, Hasbrouck 1991, Kondor 2012 — first-guess DOIs | Crossref | each first guess resolved to a *different* paper; all corrected by Crossref bibliographic query and re-verified (DOIs in table above) |
added docs/model_cards.md +84 −0
@@ -0,0 +1,84 @@
1+# Model cards — WP 27
2+
3+One card per model or provider used or planned. Prices are USD per 1M tokens (input / cached input / output) as entered in `LLMCfg.price_assumptions` (`experiments/config.py`); source: provider pricing pages fetched 2026-10-27 (OpenAI) and Anthropic's published price table. The budget tracker (`llm/budget.py`) prices every call from this table; unknown model ids are priced at the most expensive known output price. Every run manifest stores the table it used.
4+
5+## 1. gpt-5.4-nano (OpenAI) — primary model
6+
7+| Field | Value |
8+|---|---|
9+| Model id / snapshot | `gpt-5.4-nano` (alias; the dated snapshot returned in `response.model` is recorded per decision via `request_id` and the manifest) |
10+| Price | 0.20 / 0.02 / 1.25 |
11+| Measured usage (2026-10-27, prompt v1) | ≈ 760 input, ≈ 230 output tokens per decision, of which ≈ 150 reasoning tokens at `reasoning.effort = low` → ≈ 0.00044 USD per decision (planning figure 0.0005) |
12+| API / format | Responses API; `text.format = {type: json_schema, name: trading_decision, strict: true}`; `instructions` = frozen system prompt; `store = False`; `prompt_cache_key = ma-v1-<prompt_id>` |
13+| Reasoning / sampling | `reasoning.effort = low`; `temperature` not sent (rejected by the gpt-5 family); no seed parameter |
14+| Known non-determinism | Sampling at the provider; identical prompts may yield different decisions across calls; replay is exact only from the SQLite cache |
15+| Data retention | `store=False`: responses are not kept in the account's response store; OpenAI's standard abuse-monitoring retention applies (no zero-data-retention agreement) |
16+| Role in design | All dose–response, mechanism and safeguard cells; single-prompt baseline of the diversity arm; source of the frozen library |
17+| Status | **Live** (key valid 2026-10-27) |
18+
19+## 2. gpt-5.4-mini (OpenAI) — second model for the mix arm
20+
21+| Field | Value |
22+|---|---|
23+| Model id | `gpt-5.4-mini` |
24+| Price | 0.75 / 0.075 / 4.50 |
25+| Usage assumption | Same token profile as nano (760 / 230) → ≈ 0.0016 USD per decision; **TBD** from a one-seed check before T2 (reasoning tokens may differ) |
26+| API / format / reasoning | As gpt-5.4-nano |
27+| Role | `mini_25` (model-quality control), `mix_25` (two-model mix), `mini_25_equalusd` (fairness) |
28+| Status | Live-capable; not yet called; note the engineering item on mixing two OpenAI models in one run (`protocol.md` §5.2 note ii) |
29+
30+## 3. gpt-5-nano (OpenAI) — cheap alternative / robustness
31+
32+| Field | Value |
33+|---|---|
34+| Model id | `gpt-5-nano` |
35+| Price | 0.05 / 0.005 / 0.40 |
36+| Usage assumption | 760 / 230 → ≈ 0.00013 USD per decision (TBD) |
37+| API / format / reasoning | As gpt-5.4-nano |
38+| Role | Optional "previous-generation" replication of `share_25` if budget remains after T6; not in the 40 USD plan |
39+| Status | Live-capable; not planned in the confirmatory tiers |
40+
41+## 4. claude-haiku-5-5 (Anthropic) — planned cross-provider model
42+
43+| Field | Value |
44+|---|---|
45+| Model id | `claude-haiku-5-5` |
46+| Price (Anthropic published table) | 0.10 / 0.01 / 0.50 |
47+| Usage assumption | 760 / 230 → ≈ 0.00019 USD per decision (TBD; no reasoning-token accounting in the adapter) |
48+| API / format | Messages API; `output_config.format = {type: json_schema, schema}`; `output_config.effort = low`; system prompt with `cache_control: ephemeral`; no `thinking` parameter sent; `stop_reason = refusal` mapped to an invalid decision |
49+| Known non-determinism | Provider sampling; no seed |
50+| Data retention | Anthropic standard API retention (no zero-data-retention agreement) |
51+| Role | Cross-provider replication of `share_25` and a nano + haiku mix cell |
52+| Status | **Blocked** — the Anthropic key available on 2026-10-27 is rejected (HTTP 401 invalid x-api-key). The adapter is implemented and the cells are dry-run with the mock provider mapped to this model id; they enter the design only with a valid key and approved budget |
53+
54+## 5. claude-sonnet-5-5 (Anthropic) — planned frontier-model cell
55+
56+| Field | Value |
57+|---|---|
58+| Model id | `claude-sonnet-5-5` |
59+| Price (Anthropic published table) | 2.00 / 0.20 / 10.00 |
60+| Usage assumption | 760 / 230 → ≈ 0.0038 USD per decision (TBD); one `share_25` run ≈ 2.6 USD |
61+| API / format / retention | As claude-haiku-5-5 |
62+| Role | At most a 2-seed "frontier model" comparison of `share_25`; outside the 40 USD plan |
63+| Status | **Blocked** (same key issue); mock dry-run only |
64+
65+## 6. mock-v1 (local) — deterministic stand-in
66+
67+| Field | Value |
68+|---|---|
69+| Implementation | `llm/providers/mock.py`: parses the canonical user message (mid, public PV, private signal, holdings, free cash) and applies a seeded value-plus-noise rule; 60 % limit / 40 % market orders; `invalid_rate` = 5 % (half non-JSON, half schema-invalid); `herd_bias` knob adds a common tilt to every mock agent |
70+| Price | 0 / 0 / 0 (tokens approximated by word counts) |
71+| Determinism | Fully seeded (`PCG64(seed)`); reproducible |
72+| Role | Dry-run of every cell; CRN and validation tests; σ floors; `herd_bias` sweeps as a sanity check of the coordination metrics |
73+| Status | Live (local). **Not evidence about LLM behaviour** |
74+
75+## 7. library (local) — frozen nearest-neighbour replay
76+
77+| Field | Value |
78+|---|---|
79+| Implementation | `llm/library.py`: 7 standardized features of the observation (mispricing vs public PV, momentum, signal, inventory and cash ratios, spread, open-order flag); returns the recorded decision of one of the 5 nearest neighbours (seeded draw), limit price rescaled to the current mid |
80+| Source | Valid decisions recorded in the T1 gpt-5.4-nano runs (`results/library/nano_v1.npz`) |
81+| Price | 0 |
82+| Role | Scale arm at N = 1,000 and the "cached" side of the cached-vs-online comparison; runs are labelled provider `library` |
83+| Caveat | Not an adaptive agent; out-of-support at large N must be reported (distance diagnostics, `protocol.md` §5.4) |
84+| Status | Implemented; unused until T1 exists |
added docs/novelty_positioning.md +39 −0
@@ -0,0 +1,39 @@
1+# Novelty positioning — WP 27 "The Invisible Hand of Machines"
2+
3+Prepared 2026-10-27 by the literature-review member. All citation keys refer to `paper/references.bib`; every item was verified (see `docs/literature_ledger.md`).
4+
5+## 1. What the existing LLM-and-markets literature does
6+
7+**(i) Single-agent trading benchmarks and backtests.** FinMem (`yu2023finmem`), TradingGPT (`li2023tradinggpt`), FinAgent (`zhang2024finagent`) and the InvestorBench family evaluate one LLM agent (sometimes a small team) against a historical price path. The market does not react to the agent; the object is the agent's P&L or Sharpe ratio. Lopez-Lira and Tang (`lopezlira2023chatgpt`) is the return-predictability analogue (ChatGPT headline scores predict next-day returns). These papers say nothing about equilibrium prices because the price series is exogenous, and Lopez-Lira, Tang and Zhu (`lopezlira2025memorization`) show that such backtests are contaminated by memorization of the historical period.
8+
9+**(ii) LLM-populated market simulators without causal identification.** ASFM (`gao2024asfm`), StockAgent (`zhang2024stockagent`), TwinMarket (`yang2025twinmarket`, NeurIPS 2025), FCLAgent (`hashimoto2025`), StockSim (`papadakis2025`) and Lopez-Lira's open-source simulator (`lopezlira2025trade`) all place many LLM agents on a matching engine and report emergent bubbles, crashes, or reactions to macro shocks. They are descriptive: they show that LLM markets *can* produce certain patterns, usually for one population composition and a handful of seeds. Two of them already contain observations close to our mechanism — TwinMarket notes that agents driven by one model family are more homogeneous and links this to herd behaviour, and StockAgent reports systematic differences between GPT- and Gemini-driven agents — but neither randomizes the LLM share, controls the information set, or separates the fundamental value from the efficient price and the transaction price. Lopez-Lira (`lopezlira2025trade`) is the closest in spirit (persistent order book, heterogeneous roles, perturbation analysis "like partial-dependence plots"), yet the perturbations are over market conditions, not over the penetration of LLM agents, and there is no matched-seed design.
10+
11+**(iii) Experimental-finance replications with LLM subjects.** Henning et al. with Camerer (`henning2025`) run Smith–Suchanek–Williams markets with LLM traders and find near-fundamental pricing and muted bubbles; Ouyang and Sui (`ouyang2026`) find disposition effects and extrapolation in an open call auction and show prompt interventions move bubble size; Saxena, Pangallo, Hommes, Caccioli and del Rio-Chanona (`saxena2026`) test 15 models and find everything from fundamental pricing to human-like bubbles, with mixed populations unstable; Struski et al. (`struski2026`) and Chupilkin (`chupilkin2026`) replicate Smith (1962) double auctions and show slower convergence and strong dependence on the trading institution. These are the right benchmark for *behavioural fidelity* (do LLMs behave like humans?) but they hold the population 100 % LLM (or 100 % human) and ask a different question from ours.
12+
13+**(iv) Homogeneity, herding and monoculture.** The algorithmic-monoculture literature (`kleinberg2021`, `bommasani2022`, `bommasani2021`) and the output-diversity evidence (`doshi2024`, `park2024brm`, `anderson2024`) establish that shared models homogenize decisions. In LLM multi-agent systems, Cho et al. (`cho2025`) document peer conformity. The single paper that measures correlated trading from shared inputs is Song (`song2026`, ICLR 2026 workshop): overlap in retrieved documents raises pairwise action correlation among RAG trading agents from 0.04 to 0.41. That is a backtest on crypto data with no market feedback — the price does not respond to the agents — so it cannot speak to price informativeness or liquidity spirals. Hansen and Lee (`hansen2025`, Fed FEDS) re-create herding experiments with LLMs and find less herding than humans unless agents are told to maximize profit.
14+
15+**(v) AI collusion and RL agents in markets.** `calvano2020`, `klein2021`, `fish2024` (LLM pricing agents reach supracompetitive prices; prompt wording matters), `agrawal2025` (LLM sellers in CDAs collude more with communication), `colliard2026` (Q-learning market makers quote markups) and `dou2025` (RL informed speculators in a Kyle market lower price informativeness through "homogenized learning biases"). Dou, Goldstein and Ji is the nearest *theoretical* neighbour: it also ties a common learning process to reduced price efficiency. Its mechanism is strategic (price-trigger punishment, over-pruning bias in Q-learning), not a shared-error structure in signals, and it has no LLMs.
16+
17+**(vi) Policy literature.** Gensler and Bailey (`gensler2020`), the FSB (`fsb2024`), Danielsson and Uthemann (`danielsson2025`), Aldasoro et al. (`aldasoro2025`) and Meng and Chen (`meng2026`) all argue that common models and data sources will raise trading correlations and amplify stress. None provides a controlled experiment; `meng2026` offers a reduced-form 13F estimate.
18+
19+## 2. The gap this paper fills
20+
21+1. **Exogenous, randomized LLM penetration with matched seeds.** We treat `llm_share` as a treatment assigned to trader slots by an independent random stream, holding every other stream (fundamentals, news, policy assignment, activation, tie-breaks) fixed across the pair. Differences in outcomes between φ = 0 and φ > 0 are therefore causal effects of LLM penetration on the *same* market history, estimated with ≥ 30 seeds and a power calculation for the confirmatory design. No existing LLM-market paper does this; the simulators in (ii) report single compositions and the experiments in (iii) compare 100 %-LLM to 100 %-human markets.
22+
23+2. **Three prices, not one.** We separate the latent fundamental V_t (known to the simulator only), the public value V^pub_t (Kalman/HMM posterior on announced dividends and news) and the transaction price, and we compute Hasbrouck's efficient-price / pricing-error decomposition (`hasbrouck1993`), variance ratios (`lo1988`), Kyle lambda (`kyle1985`), forecasting-regression informativeness (`bai2016`) and information shares (`hasbrouck1995`) on simulated data where the truth is observed. Existing LLM simulators report returns, volatility and "bubbles" relative to a dividend-discount value but do not distinguish information-efficiency loss from transitory pressure.
24+
25+3. **A shared-model correlated-error mechanism, derived and then tested by intervention.** The theory section nests a static Kyle/Hellwig market in which each trader's signal error has a model-specific common component η_g; the share φ_g using model g and the effective number of models M_eff = 1/Σφ_g² pin down price informativeness, order-flow correlation and pricing error in closed form, with feedback (trend demand) and leverage (margin) extensions giving amplification 1/(1−λγ) and 1/(1−λX/m). The correlated-signal microstructure literature (`foster1996`, `back2000`, `goldstein2015`, `pasquariello2007`) has the ingredients but never applies them to shared foundation models, and the monoculture literature (`kleinberg2021`, `bommasani2022`) has the homogenization idea without a market-clearing price. The simulator then *intervenes* on the mechanism directly: same vs. diverse prompts (H4), provider mix, communication cohorts (H5), memory, and frozen decision-library replay vs. online calls — so M_eff is manipulated, not merely observed. Song (`song2026`) manipulates information overlap but without price feedback; TwinMarket and StockAgent observe model-family homogeneity without manipulating it.
26+
27+4. **Fairness designs by token, USD and decision-opportunity budgets, with matched latency.** Because LLM agents decide every 20 ticks and incur latency, naive comparisons confound "being an LLM" with "being slow and infrequent". We report realized LLM shares of agents, orders and volume, and run budget-matched treatments (equal tokens, equal dollars, equal decision opportunities) and matched-latency treatments. No prior LLM-market study controls for these; most do not even report the realized share of volume.
28+
29+5. **Leverage × homogeneity interaction and policy counterfactuals.** H3 (bubble/crash frequency and liquidity spirals rise in φ × leverage) connects the LLM literature to `brunnermeier2009`, `thurner2012` and `kirilenko2017`; H6 evaluates circuit breakers and position limits by crash depth and time-to-absorb, in the spirit of `budish2015`. The policy papers in (vi) call for exactly this evidence.
30+
31+## 3. Where we must be honest
32+
33+- **The idea that shared models correlate trading is not ours.** It is in `gensler2020`, `fsb2024`, `danielsson2025`, `dou2025` (as "homogenized learning biases") and empirically in `song2026`. Our contribution is identification and mechanism, not the hypothesis. The introduction should say so.
34+- **Model-family homogeneity inside LLM markets has already been observed** (`yang2025twinmarket`, `zhang2024stockagent`, `saxena2026`). We turn an observation into a treatment.
35+- **LLM-populated order-book simulators exist and are open source** (`lopezlira2025trade`, `papadakis2025`, `gao2024asfm`). Our simulator is not the novelty; the design is. We should cite and, where feasible, reuse their validation metrics (`vyetrenko2020`).
36+- **Behavioural fidelity is contested.** `henning2025` finds LLMs more fundamental-driven than humans; `ouyang2026` and `saxena2026` find bubbles; `hansen2025` finds less herding. Our results on informativeness do not require LLMs to be human-like — they require only that errors be correlated within a model — but we must not claim external validity for human-plus-LLM markets without caveats.
37+- **Prompt sensitivity and drift** (`fish2024`, `chen2024hdsr`) threaten replicability. The frozen decision-library treatment and the mock provider are our answer; the limitation remains for online runs.
38+- **Scale.** TwinMarket runs thousands of agents; our N = 10,000-slot runs replace only a fraction with online LLM calls and use the decision library for the rest. We should report this clearly rather than imply fully online populations.
39+- **The stylized model is a one-period Kyle variant with a fixed response intensity β.** The CARA extension relaxes it, but the strategic effects in `dou2025` and `colliard2026` (learning to collude, markups) are outside our theory and we should not claim to nest them.
added docs/protocol.md +96 −0
@@ -0,0 +1,96 @@
1+# Run protocol — WP 27
2+
3+Version 1.0, 2026-10-27. Operational companion to `identification.md`. Every number marked TBD is filled by the pilot and recorded in the freeze file.
4+
5+## 1. Environment
6+
7+- Project root: `~/Desktop/Recherche-UQO/finance/invisible-hand-of-machines`; Python ≥ 3.12 in `.venv` (uv lock `uv.lock`); package `market_agents` with entry point `ma` (`.venv/bin/ma`, equivalently `.venv/bin/python -m market_agents.cli`).
8+- Keys in `.env` only (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`), loaded by `ma` via `python-dotenv`; never in configs or manifests. Status 2026-10-27: OpenAI key live; **Anthropic key rejected (HTTP 401 invalid x-api-key)** — Claude cells run with the mock provider only until a valid key is installed.
9+- OpenAI calls use the Responses API with `store=False`, `text.format = json_schema (strict)`, `reasoning.effort = low`, `prompt_cache_key` per prompt id. Anthropic calls (when enabled) use the Messages API with `output_config.format = json_schema`, `effort = low`, system prompt marked for prompt caching.
10+- Hard limits: project-wide live spend ≤ 40 USD; pilot ≤ 5 USD; per-run `population.llm.usd_cap` set to ≥ 2× the expected run cost so that no confirmatory run halts for budget.
11+- Results: `results/runs/<design>/<cell>/seed<NNNN>/` (config.yaml, manifest.json, ticks/trades/agents/llm_decisions parquet, outcomes.json) and `results/runs/<design>/outcomes.parquet`. Caches: `results/cache/<design>.sqlite` (one per design; the cache is also the recorded decision library).
12+
13+## 2. Commands
14+
15+```bash
16+.venv/bin/python -m pytest tests # 23 tests (unit, property, integration) must pass
17+.venv/bin/ma run configs/baseline.yaml --seed 1 --out results/runs/baseline/seed0001 # one classical run (~0.5 s)
18+.venv/bin/ma budget configs/<design>.yaml --tokens-in 760 --tokens-out 230 # calls and USD before any live design
19+.venv/bin/ma design configs/<design>.yaml --workers 8 --cache results/cache/<design>.sqlite
20+.venv/bin/ma design configs/<design>.yaml --cells share_25 --max-seeds 1 --cache ... # single cell / single seed
21+```
22+
23+`ma design` runs mock cells in parallel processes (`--workers`) and any cell with a live provider sequentially with a shared cache (the worker count is ignored for API cells). Existing `outcomes.json` files are reused, so an interrupted design resumes without re-calling the API.
24+
25+## 3. Phase 5a — mock dry-run (no cost)
26+
27+1. Apply the gate-4 fixes listed in `research_plan.md` §3 (information parity, activation stream, confirmatory fundamentals, shock size, first commit). Re-run the test suite.
28+2. `ma design configs/exploratory.yaml --workers 8` — all 12 cells × seeds 1–3 with the mock provider (`usd_cap: 0`). Check: every cell completes; `llm_share_traders` equals the slot share (minus market makers at 100 %); `llm_valid_rate` ≈ 0.95 (the mock injects 5 % invalid answers); `n_llm_decisions` = 27–28 per LLM agent; `n_halts` > 0 only in `breaker_25`; `reject_ratio` rises in `poslimit_25`; `ret_corr` and `spillover_dy` defined in `k2_25`.
29+3. Dry-run the Anthropic cells with the mock provider mapped to the Claude model ids (`provider_mix: {mock: 1.0}`, `models: {mock: claude-haiku-5-5}`) to exercise the price table and manifests; label `treatment.provider: anthropic-mock`.
30+4. Free σ estimates: run `share_0` for seeds 1–30 (classical, ≈ 15 s) to obtain the control SD of every outcome.
31+
32+## 4. Phase 5b — live pilot (OpenAI, ≤ 5 USD)
33+
34+| Cell | `llm_share` | Model / prompt | Seeds | Decisions per run | Planned USD (0.0005/decision) |
35+|---|---|---|---|---|---|
36+| share_0 | 0 | — (control, free) | 1–3 | 0 | 0 |
37+| share_5 | 0.05 | gpt-5.4-nano, single | 1–3 | 135 | 0.20 |
38+| share_25 | 0.25 | gpt-5.4-nano, single | 1–3 | 675 | 1.01 |
39+| share_50 | 0.50 | gpt-5.4-nano, single | 1–3 | 1,350 | 2.03 |
40+| **Total** | | | | 6,480 | **3.24** (measured rate 0.00044 → 2.85) |
41+
42+Design file: `configs/pilot_openai.yaml` (per-run `usd_cap` 1.5; add the free `share_0` cell so the parquet is self-contained). Steps: `ma budget` → confirm ≤ 5 USD → `ma design configs/pilot_openai.yaml --cache results/cache/pilot_openai.sqlite` → record in `results/manifests/pilot.json`: total USD, tokens in/out/reasoning per decision, latency, validity rate, failure rate, cache-hit share, realized order and volume shares, and the SD of paired differences (vs `share_0`) for every primary outcome.
43+
44+Pilot pass criteria: validity ≥ 80 %, failures ≤ 10 %, cost per decision within ±30 % of 0.0005 USD, no budget halts. Failure → one prompt revision (v2), mock re-run, one-seed re-pilot (≤ 0.7 USD). Pilot seeds 1–3 are excluded from confirmatory estimates.
45+
46+## 5. Phase 6–7 — freeze and confirmatory design
47+
48+### 5.1 Freeze (before the first confirmatory call)
49+
50+`results/manifests/freeze.json` records: git revision; SHA of `prompting.py`, `schema.py`, `outcomes.py`; `config_hash` of the confirmatory base; cell list; seed lists (confirmatory seeds 101–132, used in order); thresholds (bubble 5 %/10 ticks, crash 10 %/20 ticks; sensitivity grid 3/5/8 % and 7/10/15 %); the σ̂_d table from the pilot; the tier budget below; model ids and prices. Confirmatory base = `configs/baseline.yaml` with design-level overrides `fundamentals.jump_prob: 1.0`, `fundamentals.jump_sigma: 4.0` (provisional — chosen so that SD(V^pub/V − 1) ≈ 0.5 %; confirm at freeze), `shocks: [{time: 300, kind: fundamental_jump, size: 10.0}, {time: 450, kind: placebo}]` (+2.1 % on V).
51+
52+### 5.2 Cells and budget by tier (gpt-5.4-nano unless stated; 27 decisions per LLM agent per run; planning cost 0.0005 USD per nano decision, 0.0016 per gpt-5.4-mini decision — the mini token profile is TBD and assumed equal to nano's)
53+
54+| Tier | Cell | Overrides (on the confirmatory base) | Seeds | Live decisions | USD |
55+|---|---|---|---|---|---|
56+| T1 primary | share_0 | `population.llm_share: 0.0` | 101–130 | 0 | 0 |
57+| | share_25 | `llm_share: 0.25` | 101–110 | 6,750 | 3.38 |
58+| | share_50 | `llm_share: 0.50` | 101–110 | 13,500 | 6.75 |
59+| T2 diversity | diverse_25 | `llm_share: 0.25, llm.prompt_set: diverse` | 101–106 | 4,050 | 2.03 |
60+| | mix_25 | `llm_share: 0.25, llm.provider_mix: {openai: 1.0}` with a second OpenAI model id for half the slots (see note) | 101–106 | 2,028 nano + 2,022 mini | 4.25 |
61+| | mini_25 | `llm_share: 0.25, llm.models.openai: gpt-5.4-mini` | 101–104 | 2,700 mini | 4.32 |
62+| T3 dose shape | share_1, share_5, share_10 | `llm_share: 0.01 / 0.05 / 0.10` | 101–104 | 1,728 | 0.86 |
63+| | share_75, share_100 | `llm_share: 0.75 / 1.00` (95 eligible slots at 100 %) | 101–102 | 9,180 | 4.59 |
64+| T4 mechanisms (φ = 0.25) | memory_25 | `llm.memory: 5` | 101–103 | 2,025 | 1.01 |
65+| | channel_25 | `llm.channel: true` (cohorts of 5) | 101–103 | 2,025 | 1.01 |
66+| | leverage_25 | `population.mix.leveraged: 0.19, trend: 0.06` (+ free `leverage_0` control at φ = 0 for the E5 interaction) | 101–103 | 2,025 | 1.01 |
67+| | breaker_25 | `exchange.halt_threshold: 0.05` (10-tick window, 20-tick halt) | 101–103 | 2,025 | 1.01 |
68+| | poslimit_25 | `exchange.position_limit: 300` | 101–102 | 1,350 | 0.68 |
69+| T5 fairness | mini_25_equalusd | mini_25 with `llm.usd_cap` = mean nano spend of share_25 (TBD ≈ 0.34) → decisions stop at equal USD | 101–103 | ≤ 630 mini | ≤ 1.01 |
70+| T6 extensions | k2_25 | two assets, `factor_loading: [0.7, 0.7]`, `statarb: 0.05` | 101–102 | 1,350 | 0.68 |
71+| | library_1000 | `population.n_traders: 1000`, `llm.frozen_library: results/library/nano_v1.npz`, provider `library` | 101–110 | 0 | 0 |
72+| | auction_25, interval_10 | call auction every 5 ticks; `decision_interval: 10` — **mock only** | 1–10 | 0 | 0 |
73+| **Confirmatory total** | | | | ≈ 66,000 | **≈ 33.3** |
74+| Pilot (§4) | | | | 6,480 | 3.24 |
75+| Contingency (retries, longer outputs) | | | | | ≈ 3.5 (≈ 9 %) |
76+| **Project cap** | | | | | **40.00** |
77+
78+Notes. (i) Equal-decisions fairness is the default (`decision_cap` unset: every LLM agent gets the same 27 opportunities); equal-USD uses the per-run `usd_cap` and therefore applies to single-provider cells only — the code has no per-provider cap. (ii) The two-model mix: `provider_mix` keys are providers, not models, and `models` maps one model per provider; mixing two OpenAI models in one run requires either a second provider alias (e.g. `openai_mini`) or a per-slot model list — a gate-4 engineering item; until then `mix_25` is dry-run with `{mock: 0.5, openai: 0.5}`. (iii) Claude cells (haiku_25 at ≈ 0.19 USD per 1,000 decisions; sonnet at ≈ 3.8 USD per 1,000) are planned only if a valid key *and* additional budget are approved before the freeze; they are not in the 40 USD plan. (iv) `ma budget` reproduces this table with `--tokens-in 760 --tokens-out 230`.
79+
80+### 5.3 Execution order and order of sacrifice
81+
82+Tiers run T1 → T6; spend is reconciled after each tier (`sum(llm_usd)` over `outcomes.parquet` plus the provider dashboard). If the cumulative spend exceeds the plan by > 10 %, seeds are cut in this fixed order: T6 → T5 → T3 (share_75/100 first) → T4 poslimit → T2 mini_25 (E3(b) then unavailable) → T4 others. T1 is never cut below 8 seeds. Nothing is added after seeing confirmatory results.
83+
84+### 5.4 Frozen-library replay (T6)
85+
86+Build the library from the T1 nano runs: `DecisionLibrary.from_runs([...T1 run dirs...]).save("results/library/nano_v1.npz")`. Replay at N = 1,000 with `activity` scaled by 100/N (as in `scripts/11_scale_benchmark.py`) so order flow per tick is constant. Report the nearest-neighbour distance distribution; if the median standardized distance at N = 1,000 exceeds the 90th percentile observed at N = 100, the arm is reported as out-of-support, not as evidence.
87+
88+## 6. Replication checklist
89+
90+1. `git log -1` matches `freeze.json`; `pytest` passes (23 tests).
91+2. `ma design` of each tier reproduces `outcomes.parquet` from the cache (`--cache` path, `llm_cached_hits` = 1.0, zero USD).
92+3. Every run directory has `manifest.json` with `config_hash`, `git_revision`, `prompt_version`, `price_assumptions`, `llm_usage`, `shocks`, `final_conservation_ok`.
93+4. Analysis scripts read only `outcomes.parquet`, `ticks.parquet`, `trades.parquet`, `llm_decisions.parquet`; tables and figures are regenerated by one command (`paper/Makefile`).
94+5. The sensitivity grid (bubble 3/5/8 %, crash 7/10/15 %) is produced by re-running `run_outcomes` with the alternative thresholds on saved ticks, not by re-simulating.
95+6. Decision logs are published without raw prompts if they contain nothing private (they do not: the prompt is a canonical market snapshot); model ids, dates and prices are listed in `model_cards.md`.
96+7. Deviations from this protocol are listed in the paper's appendix with the date and reason.
added docs/research_plan.md +63 −0
@@ -0,0 +1,63 @@
1+# Research plan — UQO Working Paper 27
2+## *The Invisible Hand of Machines: Price Discovery in Markets Populated by LLM Agents*
3+
4+Methodology document (causal inference and experimental design). Version 1.0, 2026-10-27. Companion documents: `identification.md` (pre-analysis plan), `protocol.md` (run protocol and budget), `assumptions.md` (assumption register), `model_cards.md`. The simulator is `src/market_agents`; what it implements is summarised in `DESIGN-BRIEF.md`.
5+
6+## 1. Primary contribution
7+
8+A randomized, pre-registered simulation experiment that identifies the *causal* effect of replacing a share of human-like heuristic traders by LLM-driven traders on price discovery, liquidity and stability in a continuous double auction. The identification strategy is experimental: treatment (which trader slots become LLM-controlled, which model/prompt they use, which market safeguards apply) is assigned by dedicated seed streams, while fundamentals, news, endowments and latencies are held fixed within matched seeds (common random numbers). The stylized model in the brief (shared model error η_g, effective number of models M_eff = 1/Σφ_g²) supplies the directional hypotheses; the simulator tests them.
9+
10+What is new relative to the LLM-agent-market literature: (i) explicit control arms and matched pairs instead of single demonstrations; (ii) dose–response in the LLM share; (iii) diversity as a treatment (same model vs diverse prompts vs model mix) at a *fixed* share; (iv) safeguards as treatments; (v) a budget-explicit, power-explicit protocol with pre-specified stopping and negative-result rules.
11+
12+## 2. Research questions and hypotheses
13+
14+| RQ | Question | Hypothesis (brief) | Lead outcome (`outcomes.py`) |
15+|---|---|---|---|
16+| RQ1 | Does price informativeness fall as the share φ of traders sharing one model rises, holding information quality fixed? | H1: `msd_log` increases in φ; falls in M_eff | `msd_log` |
17+| RQ2 | Do LLM traders produce correlated, synchronised order flow? | H2: ρ̄ and synchrony rise in φ | `llm_pairwise_corr` |
18+| RQ3 | Does the φ × leverage interaction raise bubble/crash frequency and liquidity spirals? | H3: `n_crashes`, `max_dd`, `n_bubbles` rise with φ × leverage | `n_crashes` |
19+| RQ4 | Does prompt or model diversity restore part of the loss? | H4: diverse < single on `msd_log` at equal φ | `msd_log` (diverse − single) |
20+| RQ5 | Does a communication channel raise synchrony? | H5: channel on > off on synchrony | `llm_synchrony` |
21+| RQ6 | Do circuit breakers and position limits cut crash depth at a cost in absorption time? | H6: `max_dd` falls, `time_to_absorb` rises | `max_dd` |
22+
23+Liquidity (spread, depth, Kyle λ, resiliency) is treated as a sub-family H1b of RQ1 with its own lead outcome `spread_bp`. Full outcome families, directions, thresholds and estimators: `identification.md` §3–4.
24+
25+## 3. Phases, gates and status
26+
27+| Phase | Content | Gate (must hold to proceed) | Status 2026-10-27 |
28+|---|---|---|---|
29+| 0 | Scoping: brief, economy, treatments, outcome list | Brief agreed by author, theory and methods | Done (`DESIGN-BRIEF.md`) |
30+| 1 | Engine: event-sourced LOB, accounts, fees, latency, margin, halts, dividends, Kalman public value, news | Unit + property tests pass; cash/share conservation checked after every fill | Done — 23 tests pass (`tests/unit`, `tests/property`, `tests/integration`) |
31+| 2 | Classical population (fundamentalist, trend, mean-reverter, noise, market maker, leveraged, stat-arb), stratified slot assignment, seed streams | Determinism across identical runs; matched-pair invariance of fundamentals and endowments (`test_common_random_numbers_across_llm_share`) | Done |
32+| 3 | Calibration and scale | Stylized facts reproduced (fat tails, volatility clustering, spread/depth in plausible range); run time ≤ 1 min at N = 10,000 | Done — 360-run sweep (`results/calibration/sweep.parquet`): default cell kurtosis ≈ 27, Σ ACF(abs r) ≈ 3.1, spread ≈ 12 bp, mad ≈ 3.3 %; wall time 0.5 s / 2.4 s / 22 s at N = 100 / 1,000 / 10,000 |
33+| 4 | LLM layer (OpenAI, Anthropic, mock, library), cache, budget tracker, prompts v1, decision schema; pre-analysis plan (this folder) | **Gate 4 (open):** (a) information parity fix — an LLM slot must inherit the informed/uninformed status of the slot it replaces (`population.py`, l. 105; today 30 % of *all* LLM slots are informed vs 7.5 % of classical slots: in a mock run 44 % vs 6 %); (b) activation-stream fix — `runner.py` l. 204 shuffles the active list with the activation stream, so activation draws diverge across arms after tick 0 (96 % of non-replaced agents had a different order count across arms in a mock test); (c) confirmatory fundamentals with latent uncertainty — in the baseline the dividend reveals x_t exactly, so V^pub ≡ V and private signals are redundant (H1 not identified); (d) shock size — `size: 40.0` moves V by +8.4 %, not the +2 % stated in the YAML comment (a lagged response then counts as a 5 % "bubble"); use ≈ 10 c (+2.1 %); (e) first git commit so manifests carry a `git_revision`; (f) documents of this folder reviewed | In progress: code exists, Anthropic key rejected (HTTP 401) → Claude cells mock-only |
34+| 5 | Mock dry-run of every cell (`configs/exploratory.yaml`), then live OpenAI pilot (`configs/pilot_openai.yaml`, ≤ 5 USD) | Validity rate ≥ 80 %, failure rate ≤ 10 %, cost per decision within ±30 % of 0.0005 USD, σ of paired differences estimated | Configs written, not run |
35+| 6 | Freeze: prompt v1 SHA, schema, config hashes, thresholds, estimators, seed lists, budget tiers (§`identification.md` §6) | Freeze record committed before any confirmatory call | Pending pilot |
36+| 7 | Confirmatory runs by tier (`protocol.md` §5), reconciliation of spend after each tier | Cumulative live spend ≤ 40 USD; balanced seeds within tier | Not started |
37+| 8 | Analysis: paired estimates, randomization inference, dose–response, diversity contrasts, local projections, sensitivity grid, mediation (descriptive), robustness (mock, library at N = 1,000, call auction) | All pre-registered tables produced from `results/runs/*/outcomes.parquet` by scripted code; no hand edits | Not started |
38+| 9 | Paper, replication package, model cards, data (decision logs without raw prompts) | Clean-room re-run of one tier from the frozen library reproduces tables | Literature review and theory in progress |
39+
40+## 4. What would make us abandon or revise each hypothesis
41+
42+Criteria are evaluated on the confirmatory tier for the primary contrast (25 % vs 0 %, gpt-5.4-nano, single prompt) unless stated. MDE = minimal detectable effect at the achieved seed count (≈ d = 0.9 at n = 10 pairs; see `identification.md` §5).
43+
44+| H | Revise if | Abandon (report as negative) if |
45+|---|---|---|
46+| H1 (informativeness ↓ in φ) | `msd_log` effect is positive but driven entirely by the `llm_valid_rate` or by the realized order share being < 2 % (dose too small to matter) → re-interpret as "activity" effect; or effect disappears under latent-uncertainty fundamentals but not under exact-revelation fundamentals → it is a microstructure, not an information, effect | Paired 95 % CI of Δ`msd_log` excludes d ≥ 0.5 in the predicted direction at both 25 % and 50 %, and the dose–response (Page's L) is flat |
47+| H2 (ρ̄, synchrony ↑ in φ) | ρ̄ among LLM agents is high but *not* higher than among classical agents of the replaced types (classical analogue computed from `trades.parquet`; see `identification.md` §3.4) → the correlation is a property of rule-like trading, not of model sharing | ρ̄ does not rise with φ (Page's L, p > 0.05) *and* is below the classical analogue |
48+| H3 (φ × leverage → crashes) | Crash counts are zero at the 10 % threshold in all cells (floor effect) → use the pre-registered 7 % sensitivity threshold and `max_dd` as lead | Interaction term CI excludes the predicted sign at 7, 10 and 15 % thresholds |
49+| H4 (diversity restores) | Diverse prompts help but model mix does not (or vice versa) → theory must distinguish persona-level from model-level error η_g | Neither contrast (diverse − single; mix − average of singles) has the predicted sign with CI excluding d ≥ 0.5 |
50+| H5 (channel ↑ synchrony) | Synchrony rises but `msd_log` does not → channel moves coordination without moving prices | Channel effect on `llm_synchrony` CI excludes d ≥ 0.5 |
51+| H6 (safeguards) | `max_dd` falls but `time_to_absorb` does not rise → no trade-off (report as such) | Breaker and position limit both leave `max_dd` and `n_crashes` unchanged (CI excludes d ≥ 0.5) |
52+
53+Cross-cutting abandonment: if the pilot validity rate of gpt-5.4-nano is < 80 % after one prompt revision (v2), the treatment is "an agent that mostly holds" and the study is reframed around activity rather than reasoning; if the live spend cannot deliver ≥ 8 matched seeds on the primary contrast, the confirmatory claim is downgraded to "exploratory, powered for d ≥ 1 only".
54+
55+## 5. Risks
56+
57+| Risk | Mitigation |
58+|---|---|
59+| Anthropic key invalid (confirmed 2026-10-27) | Claude cells are implemented and mock-tested; they enter the design only when a key is validated; budget tiers do not depend on them |
60+| Budget: 40 USD ≈ 70,000–80,000 nano decisions; a full factorial is impossible | Tiered design with a pre-specified order of sacrifice (`protocol.md` §5.3) |
61+| Provider non-determinism and drift | Every response cached (SQLite) with request id; replay reproduces runs exactly; model snapshot and date recorded in manifests |
62+| Realized LLM dose far below slot share (mock: 3.5 % of orders at 25 % of slots) | Report both assigned and realized dose; `decision_interval` sensitivity with the mock; interpret effects per unit of realized order share |
63+| Pilot σ from 3 pairs is imprecise | Classical control σ from 30 free seeds as floor; conservative seed rule |
added paper/.latexmkrc +5 −0
@@ -0,0 +1,5 @@
1+$pdf_mode = 1;
2+$pdflatex = 'pdflatex -interaction=nonstopmode -halt-on-error -synctex=1 %O %S';
3+$bibtex_use = 2;
4+$clean_ext = 'synctex.gz run.xml bbl bcf fdb_latexmk fls log aux out toc lof lot blg';
5+@default_files = ('main.tex');
added paper/Makefile +11 −0
@@ -0,0 +1,11 @@
1+.PHONY: all clean distclean
2+
3+all:
4+ latexmk
5+
6+clean:
7+ latexmk -c
8+
9+distclean:
10+ latexmk -C
11+ rm -f *.synctex.gz *.run.xml *.bbl *.bcf
added paper/appendices/proofs.tex +282 −0
@@ -0,0 +1,282 @@
1+% paper/appendices/proofs.tex — UQO WP 27. Proofs of the propositions of Section "A stylized model".
2+\section{Proofs}
3+\label{app:proofs}
4+
5+Throughout, A1--A4 hold and the notation is that of Section~\ref{sec:theory}:
6+$n_g=\phi_gN$, $\Phi=\sum_g\phi_g$, $H=\sum_g\phi_g^2$, $\sigma_s^2=\sigma_v^2+\sigma_\eta^2+\sigma_u^2$,
7+$\bar\varepsilon=N^{-1}\sum_i\varepsilon_i$ and $\bar Y=Y/(\beta N)$.
8+
9+\subsection*{Two lemmas}
10+
11+\begin{lemma}[Flow decomposition]
12+\label{lem:flow}
13+$Y=\beta N\,(v+\bar\varepsilon)+z$ and
14+$\operatorname{Var}(\bar\varepsilon)=\sigma_\eta^2\big[H+(1-\Phi)/N\big]+\sigma_u^2/N$. Consequently
15+$\operatorname{Var}(\bar Y-v)=\Omega$ as defined in \eqref{eq:lambda}.
16+\end{lemma}
17+
18+\begin{proof}
19+By A2, $\sum_ix_i=\beta\sum_i(v+\varepsilon_i)=\beta N(v+\bar\varepsilon)$. Let $I$ be the set of
20+traders on private models, $|I|=N(1-\Phi)$. Then
21+$\sum_i\varepsilon_i=\sum_{g=1}^Gn_g\eta_g+\sum_{i\in I}\eta_i+\sum_{i=1}^Nu_i$, a sum of
22+$G+|I|+N$ independent centered Gaussians (A1), whence
23+$\operatorname{Var}(\sum_i\varepsilon_i)=\sigma_\eta^2\sum_gn_g^2+\sigma_\eta^2N(1-\Phi)+N\sigma_u^2$.
24+Dividing by $N^2$ and using $n_g^2/N^2=\phi_g^2$ gives the variance of $\bar\varepsilon$. Finally
25+$\bar Y-v=\bar\varepsilon+z/(\beta N)$ with $z$ independent of $\bar\varepsilon$, so
26+$\operatorname{Var}(\bar Y-v)=\operatorname{Var}(\bar\varepsilon)+\sigma_z^2/(\beta^2N^2)=\Omega$.
27+\end{proof}
28+
29+\begin{lemma}[Projection]
30+\label{lem:proj}
31+$\lambda=\tau/(\beta N)$, $\tau=\sigma_v^2/(\sigma_v^2+\Omega)$, $P=\tau\bar Y$, and
32+$\operatorname{Var}(P-v)=\operatorname{Var}(v\mid Y)=\sigma_v^2(1-\tau)$.
33+\end{lemma}
34+
35+\begin{proof}
36+$(v,Y)$ is jointly Gaussian, so $\mathbb E[v\mid Y]=\lambda Y$ with
37+$\lambda=\operatorname{Cov}(v,Y)/\operatorname{Var}(Y)$ and
38+$\operatorname{Var}(v\mid Y)=\sigma_v^2-\operatorname{Cov}(v,Y)^2/\operatorname{Var}(Y)$. By Lemma~\ref{lem:flow},
39+$\operatorname{Cov}(v,Y)=\beta N\sigma_v^2$ and $\operatorname{Var}(Y)=\beta^2N^2(\sigma_v^2+\Omega)$. Hence
40+$\tau=1-\operatorname{Var}(v\mid Y)/\sigma_v^2=\operatorname{Cov}(v,Y)^2/(\sigma_v^2\operatorname{Var}(Y))
41+=\sigma_v^2/(\sigma_v^2+\Omega)$ and $\lambda=\sigma_v^2/[\beta N(\sigma_v^2+\Omega)]=\tau/(\beta N)$, so
42+$P=\lambda Y=\tau\bar Y$. For the pricing error,
43+\begin{align*}
44+\operatorname{Var}(P-v)&=\lambda^2\operatorname{Var}(Y)-2\lambda\operatorname{Cov}(v,Y)+\sigma_v^2
45+=\frac{\operatorname{Cov}(v,Y)^2}{\operatorname{Var}(Y)}-2\frac{\operatorname{Cov}(v,Y)^2}{\operatorname{Var}(Y)}+\sigma_v^2\\
46+&=\sigma_v^2-\frac{\operatorname{Cov}(v,Y)^2}{\operatorname{Var}(Y)}=\operatorname{Var}(v\mid Y),
47+\end{align*}
48+which equals $\sigma_v^2(1-\tau)$ by definition of $\tau$.
49+\end{proof}
50+
51+\subsection*{Proof of Proposition~\ref{prop:info}}
52+
53+With a single model of $n=\phi N$ users, $H=\phi^2$ and $\Phi=\phi$, so Lemma~\ref{lem:flow} gives
54+\eqref{eq:tau_phi}. Since $\tau=\sigma_v^2/(\sigma_v^2+\Omega)$ is strictly decreasing in $\Omega$, every
55+monotonicity claim about $\tau$ is the reverse claim about $\Omega$.
56+
57+\emph{(i) Discrete monotonicity.} Writing $\Omega$ as a function of the integer $n$,
58+$\Omega(n)=\sigma_\eta^2\,(n^2-n+N)/N^2+C$ with $C=\sigma_u^2/N+\sigma_z^2/(\beta^2N^2)$. Then
59+\[
60+\Omega(n+1)-\Omega(n)=\frac{\sigma_\eta^2}{N^2}\Big[(n+1)^2-(n+1)-n^2+n\Big]=\frac{2n\,\sigma_\eta^2}{N^2},
61+\]
62+which is zero for $n=0$ and strictly positive for every $n\ge1$ when $\sigma_\eta^2>0$. Hence
63+$\Omega(0)=\Omega(1)<\Omega(2)<\dots<\Omega(N)$ and $\tau$ is strictly decreasing on $\{1/N,\dots,1\}$,
64+with $\tau(0)=\tau(1/N)$. For $N=1$ the grid is $\{0,1\}$ and $\Omega(0)=\Omega(1)$. In the continuous
65+relaxation, $\Omega'(\phi)=\sigma_\eta^2(2\phi-1/N)$, which is strictly positive if and only if
66+$\phi>1/(2N)$; so $\partial\tau/\partial\phi=-\sigma_v^2\Omega'(\phi)/(\sigma_v^2+\Omega)^2<0$ exactly
67+there. (For $0<\phi<1/(2N)$ the relaxation is slightly increasing in $\phi$; no integer population lies
68+in this region, since it corresponds to fewer than one-half of a trader using the model.)
69+
70+\emph{(ii) Uniform bound and limit.} For every $N$ and $\phi\in[0,1]$,
71+$\Omega(\phi)-\sigma_\eta^2\phi^2=\sigma_\eta^2(1-\phi)/N+\sigma_u^2/N+\sigma_z^2/(\beta^2N^2)\ge0$, so
72+$\Omega(\phi)\ge\sigma_\eta^2\phi^2$ and $\tau(\phi)\le\sigma_v^2/(\sigma_v^2+\phi^2\sigma_\eta^2)=\tau_\infty(\phi)$,
73+which is strictly below one when $\phi\sigma_\eta>0$. The difference $\Omega(\phi)-\sigma_\eta^2\phi^2$ is
74+$O(1/N)$ for fixed $\phi$ and fixed $\sigma_z^2$, so $\tau(\phi)\to\tau_\infty(\phi)$.
75+
76+\emph{(iii) Private-model benchmark.} At $\phi=0$, $\Omega=(\sigma_\eta^2+\sigma_u^2)/N+\sigma_z^2/(\beta^2N^2)$,
77+which tends to zero whenever $\sigma_z^2/N^2\to0$; then $\tau\to1$. If instead noise trading scales
78+with the market, $\sigma_z^2=cN^2$, the limit is $\sigma_v^2/(\sigma_v^2+c/\beta^2)<1$ even at $\phi=0$;
79+this is the usual noisy-rational-expectations bound and is unrelated to model sharing. \qed
80+
81+\subsection*{Proof of Proposition~\ref{prop:corr}}
82+
83+\emph{(i)} By Lemma~\ref{lem:proj}, $\operatorname{Var}(P-v)=\sigma_v^2(1-\tau)=\sigma_v^2\Omega/(\sigma_v^2+\Omega)$,
84+an increasing function of $\Omega$; the monotonicity in $\phi$ and the uniform lower bound follow from
85+parts (i)--(ii) of Proposition~\ref{prop:info} since $y\mapsto\sigma_v^2y/(\sigma_v^2+y)$ is increasing.
86+
87+\emph{(ii)} By A2, $\operatorname{Corr}(x_i,x_j)=\operatorname{Corr}(s_i,s_j)$ since $\beta>0$. By A1,
88+$\operatorname{Var}(s_i)=\sigma_s^2$ for every $i$, and for $i\neq j$,
89+$\operatorname{Cov}(s_i,s_j)=\sigma_v^2+\operatorname{Cov}(\eta_{g(i)},\eta_{g(j)})$, which equals
90+$\sigma_v^2+\sigma_\eta^2$ if $g(i)=g(j)\neq\varnothing$ and $\sigma_v^2$ otherwise (two private models, or
91+two different shared models, have independent $\eta$'s). This gives $\rho_1$ and $\rho_0$, and
92+$\rho_1-\rho_0=\sigma_\eta^2/\sigma_s^2>0$ if and only if $\sigma_\eta^2>0$.
93+
94+\emph{(iii)} Among the $N(N-1)$ ordered pairs $i\neq j$, exactly $n(n-1)$ have both members on the
95+shared model; all others have correlation $\rho_0$. Hence
96+$\bar\rho=\rho_0+w(\rho_1-\rho_0)$ with $w=n(n-1)/[N(N-1)]=\phi(N\phi-1)/(N-1)$. As a function of
97+real $\phi$, $w'(\phi)=(2N\phi-1)/(N-1)$ and $w''(\phi)=2N/(N-1)>0$: $w$ is convex and increasing
98+for $\phi>1/(2N)$. On the integer grid, $w(n+1)-w(n)=2n/[N(N-1)]>0$ for $n\ge1$. As $N\to\infty$,
99+$w\to\phi^2$.
100+
101+\emph{(iv)} Let $(x_i,x_j)$ be centered jointly Gaussian with correlation $\rho\in(-1,1)$. Write
102+$x_j/\sigma_j=\rho\,x_i/\sigma_i+\sqrt{1-\rho^2}\,\xi$ with $\xi$ standard normal independent of $x_i$.
103+The classical orthant formula gives $\Pr(x_i>0,x_j>0)=\tfrac14+\arcsin(\rho)/(2\pi)$; by symmetry
104+$\Pr(x_i<0,x_j<0)$ is the same, so $S(\rho)=\Pr(x_ix_j>0)=\tfrac12+\arcsin(\rho)/\pi$.\footnote{For
105+completeness: $\Pr(x_i>0,x_j>0)=\Pr(\vartheta\in(0,\pi/2)\text{ and }\vartheta\in(\alpha-\pi/2,\alpha+\pi/2))$
106+where $\vartheta$ is the uniformly distributed angle of a standard bivariate normal vector and
107+$\cos\alpha=\rho$; the two arcs overlap on a set of measure $\pi-\alpha$, so the probability is
108+$(\pi-\arccos\rho)/(2\pi)=\tfrac14+\arcsin(\rho)/(2\pi)$.} Since $\arcsin$ is strictly increasing on
109+$[-1,1]$, so is $S$. Averaging over ordered pairs with the same count as in (iii) gives \eqref{eq:sync}.
110+Because $\bar\rho$ is affine in $w$ with slope $\rho_1-\rho_0>0$ (when $\sigma_\eta^2>0$), we may
111+regard $\bar S$ as a function of $\bar\rho$ with
112+\[
113+\frac{d\bar S}{d\bar\rho}=\frac{1}{\pi}\cdot\frac{\arcsin\rho_1-\arcsin\rho_0}{\rho_1-\rho_0}>0 .
114+\]
115+For the Jensen bound, $(\arcsin)''(\rho)=\rho(1-\rho^2)^{-3/2}\ge0$ on $[0,1)$, and $\rho_0,\rho_1\ge0$,
116+so $(1-w)\arcsin\rho_0+w\arcsin\rho_1\ge\arcsin\big((1-w)\rho_0+w\rho_1\big)=\arcsin\bar\rho$, with
117+equality if and only if $\rho_0=\rho_1$ or $w\in\{0,1\}$; for $0<w<1$ equality thus requires
118+$\sigma_\eta^2=0$.
119+
120+\emph{The placebo.} If $\sigma_\eta^2=0$ then $\Omega$ does not depend on $\phi$ (the bracket in
121+\eqref{eq:tau_phi} is multiplied by zero), $\rho_1=\rho_0$, and $w$ drops out of \eqref{eq:rhobar} and
122+\eqref{eq:sync}. Hence none of the four objects varies with $\phi$. This shows that the restriction
123+$\sigma_\eta^2>0$ is necessary for every strict claim in the proposition. \qed
124+
125+\subsection*{Proof of Proposition~\ref{prop:diversity}}
126+
127+\emph{(i)} By Lemma~\ref{lem:flow}, $\Omega=\sigma_\eta^2[H+(1-\Phi)/N]+\sigma_u^2/N+\sigma_z^2/(\beta^2N^2)$;
128+for fixed $\Phi$ the shares enter only through $H$, and $\partial\Omega/\partial H=\sigma_\eta^2>0$, so
129+$\partial\tau/\partial H=-\sigma_v^2\sigma_\eta^2/(\sigma_v^2+\Omega)^2<0$. Setting $\Phi=1$ and $H=1/M_{\mathrm{eff}}$
130+gives the stated form.
131+
132+\emph{(ii)} By the Cauchy--Schwarz inequality,
133+$\Phi^2=\big(\sum_g\phi_g\cdot1\big)^2\le\big(\sum_g\phi_g^2\big)\big(\sum_g1^2\big)=GH$, with equality if
134+and only if $(\phi_g)$ is proportional to $(1,\dots,1)$, i.e.\ $\phi_g=\Phi/G$. For the upper bound,
135+$\Phi^2=\sum_g\phi_g^2+\sum_{g\neq g'}\phi_g\phi_{g'}\ge H$, with equality if and only if all cross
136+products vanish, i.e.\ at most one share is positive. In the integer version one minimizes $\sum_gn_g^2$
137+subject to $\sum_gn_g=\Phi N$: if some $n_a\ge n_b+2$, moving one trader from $a$ to $b$ changes the
138+sum by $(n_a-1)^2+(n_b+1)^2-n_a^2-n_b^2=-2(n_a-n_b-1)<0$, so at a minimum all $n_g$ differ by at most one.
139+
140+\emph{(iii)} Combine (i) and (ii): $H_{\text{single}}=\Phi^2\ge H_{\text{mixed}}\ge\Phi^2/G=H_{\text{uniform}}$
141+and $\tau$ is decreasing in $H$. Strictness: $H_{\text{mixed}}<\Phi^2$ as soon as two shares are
142+positive, and $\partial\tau/\partial H<0$ requires $\sigma_\eta^2>0$.
143+
144+\emph{(iv)} Ordered pairs with both members on the same shared model number
145+$\sum_gn_g(n_g-1)=N^2H-N\Phi$; dividing by $N(N-1)$ gives $w=(NH-\Phi)/(N-1)$. For fixed $\Phi$, $w$
146+is increasing in $H$, and $\bar\rho$ and $\bar S$ are increasing in $w$ by the proof of
147+Proposition~\ref{prop:corr}. \qed
148+
149+\subsection*{Proof of Proposition~\ref{prop:feedback} and Corollary~\ref{cor:llmtrend}}
150+
151+Under A5 the round's price change solves $\Delta P=\lambda(Y_1+\gamma\Delta P)$.
152+
153+\emph{(i)} If $\lambda\gamma\neq1$ the unique solution is $\Delta P^\ast=\lambda Y_1/(1-\lambda\gamma)=\kappa\lambda Y_1$.
154+The iterated response satisfies $\Delta P^{(k+1)}-\Delta P^\ast=\lambda\gamma(\Delta P^{(k)}-\Delta P^\ast)$,
155+so $\Delta P^{(k)}-\Delta P^\ast=(\lambda\gamma)^k(\Delta P^{(0)}-\Delta P^\ast)$, which converges to zero
156+for every starting point if and only if $|\lambda\gamma|<1$; with $\gamma\ge0$ this is $\lambda\gamma<1$.
157+Then $\operatorname{Var}(\Delta P)=\kappa^2\lambda^2\operatorname{Var}(Y_1)$, i.e.\ amplification by
158+$\kappa^2=1/(1-\lambda\gamma)^2$ relative to the no-feedback price change $\lambda Y_1$.
159+
160+\emph{(ii)} If $\lambda\gamma=1$ the equation reads $\Delta P=\lambda Y_1+\Delta P$, impossible for
161+$Y_1\neq0$, and the iteration gives $\Delta P^{(k)}=\Delta P^{(0)}+k\lambda Y_1$, unbounded. If
162+$\lambda\gamma>1$ the algebraic solution $\Delta P^\ast=\lambda Y_1/(1-\lambda\gamma)$ exists but has the
163+sign of $-Y_1$ (a price that falls when the flow is a net purchase), and since $(\lambda\gamma)^k\to\infty$
164+the iteration diverges from every $\Delta P^{(0)}\neq\Delta P^\ast$; in particular from the natural start
165+$\Delta P^{(0)}=\lambda Y_1$. This is the counterexample announced in the text: the closed form
166+$1/(1-\lambda\gamma)^2$ is meaningless outside $\lambda\gamma<1$.
167+
168+\emph{(iii)} $\Delta P^\ast=\kappa\lambda Y_1$ is a strictly monotone function of $Y_1$ (for $\kappa>0$), so
169+$\sigma(\Delta P^\ast)=\sigma(Y_1)$ and $\operatorname{Var}(v\mid P_1)=\operatorname{Var}(v\mid Y_1)$: $\tau$ is unchanged.
170+Using $\lambda^2\operatorname{Var}(Y_1)=\lambda\operatorname{Cov}(v,Y_1)=\sigma_v^2\tau$ (Lemma~\ref{lem:proj}),
171+\[
172+\operatorname{Var}(\kappa\lambda Y_1-v)=\kappa^2\lambda^2\operatorname{Var}(Y_1)-2\kappa\lambda\operatorname{Cov}(v,Y_1)+\sigma_v^2
173+=\sigma_v^2\big[\kappa^2\tau-2\kappa\tau+1\big]=\sigma_v^2\big[(1-\tau)+\tau(\kappa-1)^2\big].
174+\]
175+On $\gamma\in[0,1/\lambda)$, $\kappa$ increases from $1$ to $+\infty$, so $(\kappa-1)^2$ is increasing in $\gamma$.
176+
177+\emph{Corollary.} Under A6 the total flow is $Y_1+\gamma_T\Delta P+\sum_i\kappa_{g(i)}\Delta P
178+=Y_1+\big(\gamma_T+\sum_gn_g\kappa_g\big)\Delta P=Y_1+\big(\gamma_T+N\sum_g\phi_g\kappa_g\big)\Delta P$,
179+with $\kappa_\varnothing\equiv0$ for private-model traders (any such loading can be folded into $\gamma_T$).
180+Applying (i)--(ii) with $\gamma=\gamma_T+N\sum_g\phi_g\kappa_g$ gives the statements; with a single
181+model, $\gamma(\phi)=\gamma_T+N\phi\kappa$ is increasing in $\phi$ when $\kappa>0$, $\kappa^2(\phi)$ is
182+increasing in $\gamma$ hence in $\phi$, and $\lambda\gamma(\phi)<1$ if and only if
183+$\phi<\phi^\ast=(1/\lambda-\gamma_T)/(N\kappa)$. If $\kappa=0$, $\gamma$ does not depend on $\phi$. With
184+several models, $\sum_g\phi_g\kappa_g$ is a share-weighted average that lies between $\Phi\min_g\kappa_g$
185+and $\Phi\max_g\kappa_g$; if the $\kappa_g$ differ in sign it can be smaller in absolute value than
186+$\Phi\kappa_g$ for every $g$. \qed
187+
188+\subsection*{Proof of Proposition~\ref{prop:spiral}}
189+
190+Let $R\equiv\{\Delta P:\Delta P<-b/X\}$ be the region in which $F>0$. On $R$,
191+$F(\Delta P)=(-X\Delta P-b)/m$ and the fixed-point equation $\Delta P=\Delta P_0-\lambda F(\Delta P)$ reads
192+\begin{equation}
193+ \Delta P=\Delta P_0+a\,\Delta P+\frac{\lambda b}{m},\qquad a=\frac{\lambda X}{m}.
194+ \label{eq:fp_affine}
195+\end{equation}
196+Off $R$, $F=0$ and the equation reads $\Delta P=\Delta P_0$.
197+
198+\emph{(i)} If $\Delta P_0\ge-b/X$, then $\Delta P=\Delta P_0\notin R$ solves the equation, and the
199+cascade defined below stops at its first step since $F(\Delta P_0)=0$. For $a<1$ it is the only fixed
200+point: a solution in $R$ would satisfy \eqref{eq:fp_affine}, whose candidate
201+$(\Delta P_0+\lambda b/m)/(1-a)$ lies in $R$ if and only if $\Delta P_0+\lambda b/m<-(b/X)(1-a)=-b/X+\lambda b/m$,
202+i.e.\ $\Delta P_0<-b/X$, which fails. For $a>1$ and $b>0$ the same algebra shows that the candidate
203+\emph{does} lie in $R$ whenever $\Delta P_0>-b/X$ (dividing by $1-a<0$ reverses the inequality): a
204+second, self-fulfilling fixed point exists in which the cohort is forced to sell because the price has
205+fallen enough to exhaust the buffer, and the price has fallen because the cohort sells. This is the
206+multiplicity familiar from \citet{brunnermeierpedersen2009}. We select the cascade limit, which is
207+$\Delta P_0$; the simulator's forced-liquidation rule is likewise sequential and cannot jump to the
208+self-fulfilling point.
209+
210+\emph{(ii)} Suppose $\Delta P_0<-b/X$ and $a<1$. The candidate $\Delta P=\Delta P_0$ is in $R$, where
211+$F>0$, so it is not a solution; the affine equation \eqref{eq:fp_affine} has the unique solution
212+$\Delta P^\ast=(\Delta P_0+\lambda b/m)/(1-a)$, which lies in $R$ by the equivalence just shown. With $b=0$
213+this is $\Delta P_0/(1-\lambda X/m)$ and the multiplier $1/(1-a)$ exceeds one. For the cascade, let
214+$\Delta P^{(0)}=\Delta P_0$ and $\Delta P^{(k)}=\Delta P_0-\lambda F(\Delta P^{(k-1)})$. Since
215+$\Delta P^{(0)}\in R$, $\Delta P^{(1)}-\Delta P^{(0)}=-\lambda F(\Delta P_0)<0$, so $\Delta P^{(1)}\in R$; by
216+induction all iterates are in $R$ and satisfy $\Delta P^{(k)}-\Delta P^\ast=a(\Delta P^{(k-1)}-\Delta P^\ast)$,
217+hence
218+\begin{equation}
219+ \Delta P^{(k)}-\Delta P^\ast=a^k(\Delta P_0-\Delta P^\ast)=-a^{k+1}\,\frac{|\Delta P_0|-b/X}{1-a},
220+ \label{eq:cascade_path}
221+\end{equation}
222+where we used $\Delta P_0-\Delta P^\ast=-\frac{\lambda}{m}\frac{X\Delta P_0+b}{1-a}=a\frac{|\Delta P_0|-b/X}{1-a}$.
223+The sequence is decreasing and converges to $\Delta P^\ast$ since $a<1$.
224+
225+\emph{(iii)} Suppose $\Delta P_0<-b/X$ and $a\ge1$. No solution exists off $R$ (it would have to be
226+$\Delta P_0\in R$). On $R$, if $a=1$ then $\lambda b/m=(b/X)(\lambda X/m)=b/X$ and \eqref{eq:fp_affine} reduces to
227+$0=\Delta P_0+b/X$, which is false because $\Delta P_0<-b/X$; if $a>1$ the unique affine solution $(\Delta P_0+\lambda b/m)/(1-a)$ is the ratio of a
228+negative numerator and a negative denominator, hence positive, so it is not in $R$. Thus there is no
229+fixed point. Along the cascade, the increments obey
230+$\Delta P^{(k+1)}-\Delta P^{(k)}=-\lambda[F(\Delta P^{(k)})-F(\Delta P^{(k-1)})]=a(\Delta P^{(k)}-\Delta P^{(k-1)})$
231+as long as the iterates remain in $R$, which they do because the first increment is negative and the
232+ratio $a$ is positive; with $a\ge1$ the increments do not shrink, so $\Delta P^{(k)}\to-\infty$. In the
233+simulator the sequence stops at the price band.
234+
235+\emph{(iv)} $\sum_{i\in L}x_i=\beta\sum_{i\in L}s_i$ is a centered Gaussian (A1--A2) with
236+\[
237+\operatorname{Var}\Big(\sum_{i\in L}s_i\Big)=\sum_{i\in L}\operatorname{Var}(s_i)+\sum_{i\neq j\in L}\operatorname{Cov}(s_i,s_j)
238+=\sigma_s^2\big[N_L+N_L(N_L-1)\bar\rho_L\big],
239+\]
240+by definition of the cohort-average correlation $\bar\rho_L$. Hence $X=|Z|$ with
241+$Z\sim\mathcal N(0,\sigma_X^2)$, $\sigma_X=\beta\sigma_s\sqrt{N_L[1+(N_L-1)\bar\rho_L]}$. Standard facts give
242+$\mathbb E|Z|=\sigma_X\sqrt{2/\pi}$ and $\Pr(|Z|\ge c)=2\bar\Phi(c/\sigma_X)$ for $c>0$, both strictly
243+increasing in $\sigma_X$, which is strictly increasing in $\bar\rho_L$ when $N_L>1$. Finally, applying
244+Proposition~\ref{prop:corr}(iii) to the cohort (with $N_L$ in place of $N$ and the cohort's own
245+shared-model share), $\bar\rho_L$ is increasing in that share on its admissible grid. The spiral
246+probability is $\Pr(a\ge1)=\Pr(X\ge m/\lambda)$, which gives the stated expression with $c=m/\lambda$.
247+
248+\emph{Remark on the net position.} If the cohort contains both longs and shorts, a negative shock forces
249+sales only from the longs, whose gross exposure $\sum_{i\in L}\max\{x_i,0\}$ is at least
250+$\max\{\sum_{i\in L}x_i,0\}$; using the net position in the spiral coefficient therefore understates
251+$a$ for a shock against the cohort's net side, and the spiral condition stated is sufficient, not
252+necessary. \qed
253+
254+\subsection*{Proof of Proposition~\ref{prop:safeguards}}
255+
256+\emph{(i)} If $X\le L<m/\lambda$ then $a=\lambda X/m\le\lambda L/m<1$ for every realization, so case (iii)
257+of Proposition~\ref{prop:spiral} cannot occur and, by case (ii), the multiplier is at most
258+$1/(1-\lambda L/m)$. For the informational cost, let $g(s)=\min\{\max\{\beta s,-\ell\},\ell\}$, which is
259+Lipschitz with $g'(s)=\beta\,\mathbf 1\{|\beta s|<\ell\}$ almost everywhere. Since $(v,s_i)$ is jointly
260+Gaussian, write $v=\theta s_i+e$ with $\theta=\operatorname{Cov}(v,s_i)/\operatorname{Var}(s_i)$ and $e$ independent of
261+$s_i$. Then $\operatorname{Cov}(v,g(s_i))=\theta\operatorname{Cov}(s_i,g(s_i))=\theta\operatorname{Var}(s_i)\mathbb E[g'(s_i)]$ by
262+Stein's lemma, i.e.\ $\operatorname{Cov}(v,\tilde x_i)=\operatorname{Cov}(v,s_i)\,\beta\Pr(|\beta s_i|<\ell)=\Pr(|\beta s_i|<\ell)\operatorname{Cov}(v,x_i)$,
263+and $\Pr(|\beta s_i|<\ell)<1$ for any finite $\ell$. We do not claim a closed form for $\tau$ under
264+clipping, since the clipped flow is non-Gaussian and the market maker's pricing rule is then nonlinear.
265+
266+\emph{(ii)} Under A7, after the halt the cascade is that of Proposition~\ref{prop:spiral} with $a$
267+replaced by $a_h\equiv\psi(h)a$ and the same starting point $\Delta P_0$ (nothing trades during the
268+halt). If $a_h<1$, part (ii) of that proposition gives convergence to
269+$\Delta P^\ast_h=(\Delta P_0+\lambda b/m)/(1-a_h)$, finite regardless of whether $a<1$. If also $a<1$,
270+then $1-a_h\ge1-a>0$ and the common numerator is negative, so $|\Delta P^\ast_h|\le|\Delta P^\ast|$, with
271+strict inequality if $\psi(h)<1$.
272+
273+\emph{(iii)} From \eqref{eq:cascade_path}, $|\Delta P^{(k)}-\Delta P^\ast|=a^{k+1}(|\Delta P_0|-b/X)/(1-a)$,
274+which is at most $\epsilon$ if and only if $k+1\ge\log\big(\epsilon(1-a)/(|\Delta P_0|-b/X)\big)/\log a$
275+(the division by $\log a<0$ reverses the inequality). Hence
276+$T_\epsilon(a)=\max\{0,\lceil\log(\epsilon(1-a)/(|\Delta P_0|-b/X))/\log a\rceil-1\}$, which is the stated
277+formula when $b=0$. Under the halt, rounds $1,\dots,h$ leave the price at $\Delta P_0$ and the cascade
278+with coefficient $a_h$ then starts from $\Delta P_0$, so the first round within $\epsilon$ of
279+$\Delta P^\ast_h$ is $h+T_\epsilon(a_h)$. Since $T_\epsilon\ge0$, $T^h_\epsilon\ge h$; if $\psi(h)=1$ then
280+$a_h=a$, $\Delta P^\ast_h=\Delta P^\ast$ and $T^h_\epsilon=T_\epsilon(a)+h$. When $a\ge1$ and $a_h<1$ the
281+comparison is $T^h_\epsilon<\infty=T_\epsilon(a)$, where the latter is infinite because the no-halt cascade
282+has no finite limit (Proposition~\ref{prop:spiral}(iii)). \qed
added paper/references.bib +1526 −0
@@ -0,0 +1,1526 @@
1+@incollection{lebaron2006,
2+ author = {LeBaron, Blake},
3+ title = {Agent-based computational finance},
4+ booktitle = {Handbook of Computational Economics, Volume 2: Agent-Based Computational Economics},
5+ year = {2006},
6+ pages = {1187--1233},
7+ doi = {10.1016/S1574-0021(05)02024-1},
8+ editor = {Tesfatsion, Leigh and Judd, Kenneth L.},
9+ publisher = {Elsevier},
10+ address = {Amsterdam}
11+}
12+
13+@article{lebaron1999,
14+ author = {LeBaron, Blake and Arthur, W.Brian and Palmer, Richard},
15+ title = {Time series properties of an artificial stock market},
16+ journal = {Journal of Economic Dynamics and Control},
17+ year = {1999},
18+ volume = {23},
19+ number = {9-10},
20+ pages = {1487--1516},
21+ doi = {10.1016/S0165-1889(98)00081-5}
22+}
23+
24+@article{lux1999,
25+ author = {Lux, Thomas and Marchesi, Michele},
26+ title = {Scaling and criticality in a stochastic multi-agent model of a financial market},
27+ journal = {Nature},
28+ year = {1999},
29+ volume = {397},
30+ number = {6719},
31+ pages = {498--500},
32+ doi = {10.1038/17290}
33+}
34+
35+@article{lux2000,
36+ author = {Lux, Thomas and Marchesi, Michele},
37+ title = {Volatility clustering in financial markets: A microsimulation of interacting agents},
38+ journal = {International Journal of Theoretical and Applied Finance},
39+ year = {2000},
40+ volume = {3},
41+ number = {4},
42+ pages = {675--702},
43+ doi = {10.1142/S0219024900000826}
44+}
45+
46+@article{brock1997,
47+ author = {Brock, William A. and Hommes, Cars H.},
48+ title = {A Rational Route to Randomness},
49+ journal = {Econometrica},
50+ year = {1997},
51+ volume = {65},
52+ number = {5},
53+ pages = {1059--1095},
54+ doi = {10.2307/2171879}
55+}
56+
57+@article{brock1998,
58+ author = {Brock, William A. and Hommes, Cars H.},
59+ title = {Heterogeneous beliefs and routes to chaos in a simple asset pricing model},
60+ journal = {Journal of Economic Dynamics and Control},
61+ year = {1998},
62+ volume = {22},
63+ number = {8-9},
64+ pages = {1235--1274},
65+ doi = {10.1016/S0165-1889(98)00011-6}
66+}
67+
68+@article{chiarella2002,
69+ author = {Chiarella, Carl and Iori, Giulia},
70+ title = {A simulation analysis of the microstructure of double auction markets},
71+ journal = {Quantitative Finance},
72+ year = {2002},
73+ volume = {2},
74+ number = {5},
75+ pages = {346--353},
76+ doi = {10.1088/1469-7688/2/5/303}
77+}
78+
79+@article{chiarella2009,
80+ author = {Chiarella, Carl and Iori, Giulia and Perelló, Josep},
81+ title = {The impact of heterogeneous trading rules on the limit order book and order flows},
82+ journal = {Journal of Economic Dynamics and Control},
83+ year = {2009},
84+ volume = {33},
85+ number = {3},
86+ pages = {525--537},
87+ doi = {10.1016/j.jedc.2008.08.001}
88+}
89+
90+@article{farmer2009,
91+ author = {Farmer, J. Doyne and Foley, Duncan},
92+ title = {The economy needs agent-based modelling},
93+ journal = {Nature},
94+ year = {2009},
95+ volume = {460},
96+ number = {7256},
97+ pages = {685--686},
98+ doi = {10.1038/460685a}
99+}
100+
101+@incollection{hommes2006,
102+ author = {Hommes, Cars H.},
103+ title = {Heterogeneous agent models in economics and finance},
104+ booktitle = {Handbook of Computational Economics, Volume 2: Agent-Based Computational Economics},
105+ year = {2006},
106+ pages = {1109--1186},
107+ doi = {10.1016/S1574-0021(05)02023-X},
108+ editor = {Tesfatsion, Leigh and Judd, Kenneth L.},
109+ publisher = {Elsevier},
110+ address = {Amsterdam}
111+}
112+
113+@article{hommes2021,
114+ author = {Hommes, Cars},
115+ title = {Behavioral and Experimental Macroeconomics and Policy Analysis: A Complex Systems Approach},
116+ journal = {Journal of Economic Literature},
117+ year = {2021},
118+ volume = {59},
119+ number = {1},
120+ pages = {149--219},
121+ doi = {10.1257/jel.20191434}
122+}
123+
124+@article{kirman1993,
125+ author = {Kirman, A.},
126+ title = {Ants, Rationality, and Recruitment},
127+ journal = {The Quarterly Journal of Economics},
128+ year = {1993},
129+ volume = {108},
130+ number = {1},
131+ pages = {137--156},
132+ doi = {10.2307/2118498}
133+}
134+
135+@article{lux1995,
136+ author = {Lux, Thomas},
137+ title = {Herd Behaviour, Bubbles and Crashes},
138+ journal = {The Economic Journal},
139+ year = {1995},
140+ volume = {105},
141+ number = {431},
142+ pages = {881--896},
143+ doi = {10.2307/2235156}
144+}
145+
146+@article{axtell2025,
147+ author = {Axtell, Robert L. and Farmer, J. Doyne},
148+ title = {Agent-Based Modeling in Economics and Finance: Past, Present, and Future},
149+ journal = {Journal of Economic Literature},
150+ year = {2025},
151+ volume = {63},
152+ number = {1},
153+ pages = {197--287},
154+ doi = {10.1257/jel.20221319}
155+}
156+
157+@article{franke2012,
158+ author = {Franke, Reiner and Westerhoff, Frank},
159+ title = {Structural stochastic volatility in asset pricing dynamics: Estimation and model contest},
160+ journal = {Journal of Economic Dynamics and Control},
161+ year = {2012},
162+ volume = {36},
163+ number = {8},
164+ pages = {1193--1211},
165+ doi = {10.1016/j.jedc.2011.10.004}
166+}
167+
168+@article{gode1993,
169+ author = {Gode, Dhananjay K. and Sunder, Shyam},
170+ title = {Allocative Efficiency of Markets with Zero-Intelligence Traders: Market as a Partial Substitute for Individual Rationality},
171+ journal = {Journal of Political Economy},
172+ year = {1993},
173+ volume = {101},
174+ number = {1},
175+ pages = {119--137},
176+ doi = {10.1086/261868}
177+}
178+
179+@article{farmer2005,
180+ author = {Farmer, J. Doyne and Patelli, Paolo and Zovko, Ilija I.},
181+ title = {The predictive power of zero intelligence in financial markets},
182+ journal = {Proceedings of the National Academy of Sciences},
183+ year = {2005},
184+ volume = {102},
185+ number = {6},
186+ pages = {2254--2259},
187+ doi = {10.1073/pnas.0409157102}
188+}
189+
190+@article{smith2003,
191+ author = {Smith, Eric and Farmer, J Doyne and Gillemot, László and Krishnamurthy, Supriya},
192+ title = {Statistical theory of the continuous double auction},
193+ journal = {Quantitative Finance},
194+ year = {2003},
195+ volume = {3},
196+ number = {6},
197+ pages = {481--514},
198+ doi = {10.1088/1469-7688/3/6/307}
199+}
200+
201+@article{bouchaud2002,
202+ author = {Bouchaud, Jean-Philippe and Mézard, Marc and Potters, Marc},
203+ title = {Statistical properties of stock order books: empirical results and models},
204+ journal = {Quantitative Finance},
205+ year = {2002},
206+ volume = {2},
207+ number = {4},
208+ pages = {251--256},
209+ doi = {10.1088/1469-7688/2/4/301}
210+}
211+
212+@article{cont2010,
213+ author = {Cont, Rama and Stoikov, Sasha and Talreja, Rishi},
214+ title = {A Stochastic Model for Order Book Dynamics},
215+ journal = {Operations Research},
216+ year = {2010},
217+ volume = {58},
218+ number = {3},
219+ pages = {549--563},
220+ doi = {10.1287/opre.1090.0780}
221+}
222+
223+@article{cont2014,
224+ author = {Cont, R. and Kukanov, A. and Stoikov, S.},
225+ title = {The Price Impact of Order Book Events},
226+ journal = {Journal of Financial Econometrics},
227+ year = {2014},
228+ volume = {12},
229+ number = {1},
230+ pages = {47--88},
231+ doi = {10.1093/jjfinec/nbt003}
232+}
233+
234+@book{bouchaud2018,
235+ author = {Bouchaud, Jean-Philippe and Bonart, Julius and Donier, Jonathan and Gould, Martin},
236+ title = {Trades, Quotes and Prices: Financial Markets Under the Microscope},
237+ year = {2018},
238+ doi = {10.1017/9781316659335},
239+ publisher = {Cambridge University Press},
240+ address = {Cambridge}
241+}
242+
243+@article{gould2013,
244+ author = {Gould, Martin D. and Porter, Mason A. and Williams, Stacy and McDonald, Mark and Fenn, Daniel J. and Howison, Sam D.},
245+ title = {Limit order books},
246+ journal = {Quantitative Finance},
247+ year = {2013},
248+ volume = {13},
249+ number = {11},
250+ pages = {1709--1742},
251+ doi = {10.1080/14697688.2013.803148}
252+}
253+
254+@article{ladley2012,
255+ author = {Ladley, Dan},
256+ title = {Zero intelligence in economics and finance},
257+ journal = {The Knowledge Engineering Review},
258+ year = {2012},
259+ volume = {27},
260+ number = {2},
261+ pages = {273--286},
262+ doi = {10.1017/s0269888912000173}
263+}
264+
265+@article{smith1962,
266+ author = {Smith, Vernon L.},
267+ title = {An Experimental Study of Competitive Market Behavior},
268+ journal = {Journal of Political Economy},
269+ year = {1962},
270+ volume = {70},
271+ number = {2},
272+ pages = {111--137},
273+ doi = {10.1086/258609}
274+}
275+
276+@article{smith1988,
277+ author = {Smith, Vernon L. and Suchanek, Gerry L. and Williams, Arlington W.},
278+ title = {Bubbles, Crashes, and Endogenous Expectations in Experimental Spot Asset Markets},
279+ journal = {Econometrica},
280+ year = {1988},
281+ volume = {56},
282+ number = {5},
283+ pages = {1119--1151},
284+ doi = {10.2307/1911361}
285+}
286+
287+@article{kyle1985,
288+ author = {Kyle, Albert S.},
289+ title = {Continuous Auctions and Insider Trading},
290+ journal = {Econometrica},
291+ year = {1985},
292+ volume = {53},
293+ number = {6},
294+ pages = {1315--1335},
295+ doi = {10.2307/1913210}
296+}
297+
298+@article{glosten1985,
299+ author = {Glosten, Lawrence R. and Milgrom, Paul R.},
300+ title = {Bid, ask and transaction prices in a specialist market with heterogeneously informed traders},
301+ journal = {Journal of Financial Economics},
302+ year = {1985},
303+ volume = {14},
304+ number = {1},
305+ pages = {71--100},
306+ doi = {10.1016/0304-405X(85)90044-3}
307+}
308+
309+@article{hellwig1980,
310+ author = {Hellwig, Martin F},
311+ title = {On the aggregation of information in competitive markets},
312+ journal = {Journal of Economic Theory},
313+ year = {1980},
314+ volume = {22},
315+ number = {3},
316+ pages = {477--498},
317+ doi = {10.1016/0022-0531(80)90056-3}
318+}
319+
320+@article{foster1996,
321+ author = {Foster, F. Douglas and Viswanathan, S.},
322+ title = {Strategic Trading When Agents Forecast the Forecasts of Others},
323+ journal = {The Journal of Finance},
324+ year = {1996},
325+ volume = {51},
326+ number = {4},
327+ pages = {1437--1478},
328+ doi = {10.1111/j.1540-6261.1996.tb04075.x}
329+}
330+
331+@article{back2000,
332+ author = {Back, Kerry and Cao, C. Henry and Willard, Gregory A.},
333+ title = {Imperfect Competition among Informed Traders},
334+ journal = {The Journal of Finance},
335+ year = {2000},
336+ volume = {55},
337+ number = {5},
338+ pages = {2117--2155},
339+ doi = {10.1111/0022-1082.00282}
340+}
341+
342+@article{kondor2012,
343+ author = {Kondor, Péter},
344+ title = {The More We Know about the Fundamental, the Less We Agree on the Price},
345+ journal = {The Review of Economic Studies},
346+ year = {2012},
347+ volume = {79},
348+ number = {3},
349+ pages = {1175--1207},
350+ doi = {10.1093/restud/rdr051}
351+}
352+
353+@article{goldstein2015,
354+ author = {Goldstein, Itay and Yang, Liyan},
355+ title = {Information Diversity and Complementarities in Trading and Information Acquisition},
356+ journal = {The Journal of Finance},
357+ year = {2015},
358+ volume = {70},
359+ number = {4},
360+ pages = {1723--1765},
361+ doi = {10.1111/jofi.12226}
362+}
363+
364+@article{dugast2018,
365+ author = {Dugast, Jérôme and Foucault, Thierry},
366+ title = {Data abundance and asset price informativeness},
367+ journal = {Journal of Financial Economics},
368+ year = {2018},
369+ volume = {130},
370+ number = {2},
371+ pages = {367--391},
372+ doi = {10.1016/j.jfineco.2018.07.004}
373+}
374+
375+@inproceedings{vyetrenko2020,
376+ author = {Vyetrenko, Svitlana and Byrd, David and Petosa, Nick and Mahfouz, Mahmoud and Dervovic, Danial and Veloso, Manuela and Balch, Tucker},
377+ title = {Get real: Realism metrics for robust limit order book market simulations},
378+ booktitle = {Proceedings of the First ACM International Conference on AI in Finance (ICAIF '20)},
379+ year = {2020},
380+ pages = {1--8},
381+ doi = {10.1145/3383455.3422561},
382+ publisher = {ACM}
383+}
384+
385+@article{hasbrouck1991,
386+ author = {Hasbrouck, Joel},
387+ title = {Measuring the Information Content of Stock Trades},
388+ journal = {The Journal of Finance},
389+ year = {1991},
390+ volume = {46},
391+ number = {1},
392+ pages = {179--207},
393+ doi = {10.1111/j.1540-6261.1991.tb03749.x}
394+}
395+
396+@article{hasbrouck1993,
397+ author = {Hasbrouck, Joel},
398+ title = {Assessing the Quality of a Security Market: A New Approach to Transaction-Cost Measurement},
399+ journal = {Review of Financial Studies},
400+ year = {1993},
401+ volume = {6},
402+ number = {1},
403+ pages = {191--212},
404+ doi = {10.1093/rfs/6.1.191}
405+}
406+
407+@article{hasbrouck1995,
408+ author = {Hasbrouck, Joel},
409+ title = {One Security, Many Markets: Determining the Contributions to Price Discovery},
410+ journal = {The Journal of Finance},
411+ year = {1995},
412+ volume = {50},
413+ number = {4},
414+ pages = {1175--1199},
415+ doi = {10.1111/j.1540-6261.1995.tb04054.x}
416+}
417+
418+@article{lo1988,
419+ author = {Lo, Andrew W. and MacKinlay, A. Craig},
420+ title = {Stock Market Prices Do Not Follow Random Walks: Evidence from a Simple Specification Test},
421+ journal = {Review of Financial Studies},
422+ year = {1988},
423+ volume = {1},
424+ number = {1},
425+ pages = {41--66},
426+ doi = {10.1093/rfs/1.1.41}
427+}
428+
429+@article{roll1984,
430+ author = {Roll, Richard},
431+ title = {A Simple Implicit Measure of the Effective Bid-Ask Spread in an Efficient Market},
432+ journal = {The Journal of Finance},
433+ year = {1984},
434+ volume = {39},
435+ number = {4},
436+ pages = {1127--1139},
437+ doi = {10.1111/j.1540-6261.1984.tb03897.x}
438+}
439+
440+@article{amihud2002,
441+ author = {Amihud, Yakov},
442+ title = {Illiquidity and stock returns: cross-section and time-series effects},
443+ journal = {Journal of Financial Markets},
444+ year = {2002},
445+ volume = {5},
446+ number = {1},
447+ pages = {31--56},
448+ doi = {10.1016/S1386-4181(01)00024-6}
449+}
450+
451+@article{bai2016,
452+ author = {Bai, Jennie and Philippon, Thomas and Savov, Alexi},
453+ title = {Have financial markets become more informative?},
454+ journal = {Journal of Financial Economics},
455+ year = {2016},
456+ volume = {122},
457+ number = {3},
458+ pages = {625--654},
459+ doi = {10.1016/j.jfineco.2016.08.005}
460+}
461+
462+@article{bond2012,
463+ author = {Bond, Philip and Edmans, Alex and Goldstein, Itay},
464+ title = {The Real Effects of Financial Markets},
465+ journal = {Annual Review of Financial Economics},
466+ year = {2012},
467+ volume = {4},
468+ number = {1},
469+ pages = {339--360},
470+ doi = {10.1146/annurev-financial-110311-101826}
471+}
472+
473+@article{hendershott2014,
474+ author = {Hendershott, Terrence and Menkveld, Albert J.},
475+ title = {Price pressures},
476+ journal = {Journal of Financial Economics},
477+ year = {2014},
478+ volume = {114},
479+ number = {3},
480+ pages = {405--423},
481+ doi = {10.1016/j.jfineco.2014.08.001}
482+}
483+
484+@article{brunnermeier2009,
485+ author = {Brunnermeier, Markus K. and Pedersen, Lasse Heje},
486+ title = {Market Liquidity and Funding Liquidity},
487+ journal = {Review of Financial Studies},
488+ year = {2009},
489+ volume = {22},
490+ number = {6},
491+ pages = {2201--2238},
492+ doi = {10.1093/rfs/hhn098}
493+}
494+
495+@article{shleifer1992,
496+ author = {Shleifer, Andrei and Vishny, Robert W.},
497+ title = {Liquidation Values and Debt Capacity: A Market Equilibrium Approach},
498+ journal = {The Journal of Finance},
499+ year = {1992},
500+ volume = {47},
501+ number = {4},
502+ pages = {1343--1366},
503+ doi = {10.1111/j.1540-6261.1992.tb04661.x}
504+}
505+
506+@article{shleifer1997,
507+ author = {Shleifer, Andrei and Vishny, Robert W.},
508+ title = {The Limits of Arbitrage},
509+ journal = {The Journal of Finance},
510+ year = {1997},
511+ volume = {52},
512+ number = {1},
513+ pages = {35--55},
514+ doi = {10.1111/j.1540-6261.1997.tb03807.x}
515+}
516+
517+@article{shleifer2011,
518+ author = {Shleifer, Andrei and Vishny, Robert},
519+ title = {Fire Sales in Finance and Macroeconomics},
520+ journal = {Journal of Economic Perspectives},
521+ year = {2011},
522+ volume = {25},
523+ number = {1},
524+ pages = {29--48},
525+ doi = {10.1257/jep.25.1.29}
526+}
527+
528+@article{geanakoplos2010,
529+ author = {Geanakoplos, John},
530+ title = {The Leverage Cycle},
531+ journal = {NBER Macroeconomics Annual},
532+ year = {2010},
533+ volume = {24},
534+ number = {1},
535+ pages = {1--66},
536+ doi = {10.1086/648285}
537+}
538+
539+@article{adrian2010,
540+ author = {Adrian, Tobias and Shin, Hyun Song},
541+ title = {Liquidity and leverage},
542+ journal = {Journal of Financial Intermediation},
543+ year = {2010},
544+ volume = {19},
545+ number = {3},
546+ pages = {418--437},
547+ doi = {10.1016/j.jfi.2008.12.002}
548+}
549+
550+@article{gromb2002,
551+ author = {Gromb, Denis and Vayanos, Dimitri},
552+ title = {Equilibrium and welfare in markets with financially constrained arbitrageurs},
553+ journal = {Journal of Financial Economics},
554+ year = {2002},
555+ volume = {66},
556+ number = {2-3},
557+ pages = {361--407},
558+ doi = {10.1016/S0304-405X(02)00228-3}
559+}
560+
561+@article{thurner2012,
562+ author = {Thurner, Stefan and Farmer, J. Doyne and Geanakoplos, John},
563+ title = {Leverage causes fat tails and clustered volatility},
564+ journal = {Quantitative Finance},
565+ year = {2012},
566+ volume = {12},
567+ number = {5},
568+ pages = {695--707},
569+ doi = {10.1080/14697688.2012.674301}
570+}
571+
572+@article{kyle2001,
573+ author = {Kyle, Albert S. and Xiong, Wei},
574+ title = {Contagion as a Wealth Effect},
575+ journal = {The Journal of Finance},
576+ year = {2001},
577+ volume = {56},
578+ number = {4},
579+ pages = {1401--1440},
580+ doi = {10.1111/0022-1082.00373}
581+}
582+
583+@article{cont2013,
584+ author = {Cont, Rama and Wagalath, Lakshithe},
585+ title = {Running for the exit: Distressed selling and endogenous correlation in financial markets},
586+ journal = {Mathematical Finance},
587+ year = {2013},
588+ volume = {23},
589+ number = {4},
590+ pages = {718--741},
591+ doi = {10.1111/j.1467-9965.2011.00510.x}
592+}
593+
594+@article{greenwood2015,
595+ author = {Greenwood, Robin and Landier, Augustin and Thesmar, David},
596+ title = {Vulnerable banks},
597+ journal = {Journal of Financial Economics},
598+ year = {2015},
599+ volume = {115},
600+ number = {3},
601+ pages = {471--485},
602+ doi = {10.1016/j.jfineco.2014.11.006}
603+}
604+
605+@article{lakonishok1992,
606+ author = {Lakonishok, Josef and Shleifer, Andrei and Vishny, Robert W.},
607+ title = {The impact of institutional trading on stock prices},
608+ journal = {Journal of Financial Economics},
609+ year = {1992},
610+ volume = {32},
611+ number = {1},
612+ pages = {23--43},
613+ doi = {10.1016/0304-405X(92)90023-Q}
614+}
615+
616+@article{kirilenko2017,
617+ author = {Kirilenko, Andrei and Kyle, Albert S. and Samadi, Mehrdad and Tuzun, Tugkan},
618+ title = {The Flash Crash: High-Frequency Trading in an Electronic Market},
619+ journal = {The Journal of Finance},
620+ year = {2017},
621+ volume = {72},
622+ number = {3},
623+ pages = {967--998},
624+ doi = {10.1111/jofi.12498}
625+}
626+
627+@article{menkveld2013,
628+ author = {Menkveld, Albert J.},
629+ title = {High frequency trading and the new market makers},
630+ journal = {Journal of Financial Markets},
631+ year = {2013},
632+ volume = {16},
633+ number = {4},
634+ pages = {712--740},
635+ doi = {10.1016/j.finmar.2013.06.006}
636+}
637+
638+@article{hendershott2011,
639+ author = {Hendershott, Terrence and Jones, Charles M. and Menkveld, Albert J.},
640+ title = {Does Algorithmic Trading Improve Liquidity?},
641+ journal = {The Journal of Finance},
642+ year = {2011},
643+ volume = {66},
644+ number = {1},
645+ pages = {1--33},
646+ doi = {10.1111/j.1540-6261.2010.01624.x}
647+}
648+
649+@article{budish2015,
650+ author = {Budish, Eric and Cramton, Peter and Shim, John},
651+ title = {The high-frequency trading arms race: Frequent batch auctions as a market design response},
652+ journal = {The Quarterly Journal of Economics},
653+ year = {2015},
654+ volume = {130},
655+ number = {4},
656+ pages = {1547--1621},
657+ doi = {10.1093/qje/qjv027}
658+}
659+
660+@article{bikhchandani1992,
661+ author = {Bikhchandani, Sushil and Hirshleifer, David and Welch, Ivo},
662+ title = {A Theory of Fads, Fashion, Custom, and Cultural Change as Informational Cascades},
663+ journal = {Journal of Political Economy},
664+ year = {1992},
665+ volume = {100},
666+ number = {5},
667+ pages = {992--1026},
668+ doi = {10.1086/261849}
669+}
670+
671+@article{banerjee1992,
672+ author = {Banerjee, A. V.},
673+ title = {A Simple Model of Herd Behavior},
674+ journal = {The Quarterly Journal of Economics},
675+ year = {1992},
676+ volume = {107},
677+ number = {3},
678+ pages = {797--817},
679+ doi = {10.2307/2118364}
680+}
681+
682+@article{froot1992,
683+ author = {Froot, Kenneth A. and Scharfstein, David S. and Stein, Jeremy C.},
684+ title = {Herd on the Street: Informational Inefficiencies in a Market with Short-Term Speculation},
685+ journal = {The Journal of Finance},
686+ year = {1992},
687+ volume = {47},
688+ number = {4},
689+ pages = {1461--1484},
690+ doi = {10.1111/j.1540-6261.1992.tb04665.x}
691+}
692+
693+@article{chaboud2014,
694+ author = {Chaboud, Alain P. and Chiquoine, Benjamin and Hjalmarsson, Erik and Vega, Clara},
695+ title = {Rise of the Machines: Algorithmic Trading in the Foreign Exchange Market},
696+ journal = {The Journal of Finance},
697+ year = {2014},
698+ volume = {69},
699+ number = {5},
700+ pages = {2045--2084},
701+ doi = {10.1111/jofi.12186}
702+}
703+
704+@article{brogaard2014,
705+ author = {Brogaard, Jonathan and Hendershott, Terrence and Riordan, Ryan},
706+ title = {High-Frequency Trading and Price Discovery},
707+ journal = {Review of Financial Studies},
708+ year = {2014},
709+ volume = {27},
710+ number = {8},
711+ pages = {2267--2306},
712+ doi = {10.1093/rfs/hhu032}
713+}
714+
715+@article{menkveld2016,
716+ author = {Menkveld, Albert J.},
717+ title = {The Economics of High-Frequency Trading: Taking Stock},
718+ journal = {Annual Review of Financial Economics},
719+ year = {2016},
720+ volume = {8},
721+ number = {1},
722+ pages = {1--24},
723+ doi = {10.1146/annurev-financial-121415-033010}
724+}
725+
726+@article{khandani2011,
727+ author = {Khandani, Amir E. and Lo, Andrew W.},
728+ title = {What happened to the quants in August 2007? Evidence from factors and transactions data},
729+ journal = {Journal of Financial Markets},
730+ year = {2011},
731+ volume = {14},
732+ number = {1},
733+ pages = {1--46},
734+ doi = {10.1016/j.finmar.2010.07.005}
735+}
736+
737+@article{easley2011,
738+ author = {Easley, David and López de Prado, Marcos M. and O’Hara, Maureen},
739+ title = {The microstructure of the `Flash Crash': Flow toxicity, liquidity crashes, and the probability of informed trading},
740+ journal = {The Journal of Portfolio Management},
741+ year = {2011},
742+ volume = {37},
743+ number = {2},
744+ pages = {118--128},
745+ doi = {10.3905/jpm.2011.37.2.118}
746+}
747+
748+@article{foucault2016,
749+ author = {Foucault, Thierry and Hombert, Johan and Roşu, Ioanid},
750+ title = {News Trading and Speed},
751+ journal = {The Journal of Finance},
752+ year = {2016},
753+ volume = {71},
754+ number = {1},
755+ pages = {335--382},
756+ doi = {10.1111/jofi.12302}
757+}
758+
759+@article{johnson2013,
760+ author = {Johnson, Neil and Zhao, Guannan and Hunsader, Eric and Qi, Hong and Johnson, Nicholas and Meng, Jing and Tivnan, Brian},
761+ title = {Abrupt rise of new machine ecology beyond human response time},
762+ journal = {Scientific Reports},
763+ year = {2013},
764+ volume = {3},
765+ number = {1},
766+ doi = {10.1038/srep02627},
767+ pages = {2627}
768+}
769+
770+@article{cont2001,
771+ author = {Cont, R.},
772+ title = {Empirical properties of asset returns: stylized facts and statistical issues},
773+ journal = {Quantitative Finance},
774+ year = {2001},
775+ volume = {1},
776+ number = {2},
777+ pages = {223--236},
778+ doi = {10.1080/713665670}
779+}
780+
781+@article{mandelbrot1963,
782+ author = {Mandelbrot, Benoit},
783+ title = {The Variation of Certain Speculative Prices},
784+ journal = {The Journal of Business},
785+ year = {1963},
786+ volume = {36},
787+ number = {4},
788+ pages = {394--419},
789+ doi = {10.1086/294632}
790+}
791+
792+@article{engle1982,
793+ author = {Engle, Robert F.},
794+ title = {Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation},
795+ journal = {Econometrica},
796+ year = {1982},
797+ volume = {50},
798+ number = {4},
799+ pages = {987--1007},
800+ doi = {10.2307/1912773}
801+}
802+
803+@article{bollerslev1986,
804+ author = {Bollerslev, Tim},
805+ title = {Generalized autoregressive conditional heteroskedasticity},
806+ journal = {Journal of Econometrics},
807+ year = {1986},
808+ volume = {31},
809+ number = {3},
810+ pages = {307--327},
811+ doi = {10.1016/0304-4076(86)90063-1}
812+}
813+
814+@article{fama1970,
815+ author = {Fama, Eugene F.},
816+ title = {Efficient Capital Markets: A Review of Theory and Empirical Work},
817+ journal = {The Journal of Finance},
818+ year = {1970},
819+ volume = {25},
820+ number = {2},
821+ pages = {383--417},
822+ doi = {10.2307/2325486}
823+}
824+
825+@article{ding1993,
826+ author = {Ding, Zhuanxin and Granger, Clive W.J. and Engle, Robert F.},
827+ title = {A long memory property of stock market returns and a new model},
828+ journal = {Journal of Empirical Finance},
829+ year = {1993},
830+ volume = {1},
831+ number = {1},
832+ pages = {83--106},
833+ doi = {10.1016/0927-5398(93)90006-D}
834+}
835+
836+@article{lillo2004,
837+ author = {Lillo, Fabrizio and Farmer, J. Doyne},
838+ title = {The Long Memory of the Efficient Market},
839+ journal = {Studies in Nonlinear Dynamics & Econometrics},
840+ year = {2004},
841+ volume = {8},
842+ number = {3},
843+ doi = {10.2202/1558-3708.1226},
844+ pages = {Article 1}
845+}
846+
847+@article{diebold2009,
848+ author = {Diebold, Francis X. and Yilmaz, Kamil},
849+ title = {Measuring Financial Asset Return and Volatility Spillovers, with Application to Global Equity Markets},
850+ journal = {The Economic Journal},
851+ year = {2009},
852+ volume = {119},
853+ number = {534},
854+ pages = {158--171},
855+ doi = {10.1111/j.1468-0297.2008.02208.x}
856+}
857+
858+@article{diebold2012,
859+ author = {Diebold, Francis X. and Yilmaz, Kamil},
860+ title = {Better to give than to receive: Predictive directional measurement of volatility spillovers},
861+ journal = {International Journal of Forecasting},
862+ year = {2012},
863+ volume = {28},
864+ number = {1},
865+ pages = {57--66},
866+ doi = {10.1016/j.ijforecast.2011.02.006}
867+}
868+
869+@article{diebold2014,
870+ author = {Diebold, Francis X. and Yılmaz, Kamil},
871+ title = {On the network topology of variance decompositions: Measuring the connectedness of financial firms},
872+ journal = {Journal of Econometrics},
873+ year = {2014},
874+ volume = {182},
875+ number = {1},
876+ pages = {119--134},
877+ doi = {10.1016/j.jeconom.2014.04.012}
878+}
879+
880+@article{forbes2002,
881+ author = {Forbes, Kristin J. and Rigobon, Roberto},
882+ title = {No Contagion, Only Interdependence: Measuring Stock Market Comovements},
883+ journal = {The Journal of Finance},
884+ year = {2002},
885+ volume = {57},
886+ number = {5},
887+ pages = {2223--2261},
888+ doi = {10.1111/0022-1082.00494}
889+}
890+
891+@article{kodres2002,
892+ author = {Kodres, Laura E. and Pritsker, Matthew},
893+ title = {A Rational Expectations Model of Financial Contagion},
894+ journal = {The Journal of Finance},
895+ year = {2002},
896+ volume = {57},
897+ number = {2},
898+ pages = {769--799},
899+ doi = {10.1111/1540-6261.00441}
900+}
901+
902+@article{acemoglu2015,
903+ author = {Acemoglu, Daron and Ozdaglar, Asuman and Tahbaz-Salehi, Alireza},
904+ title = {Systemic Risk and Stability in Financial Networks},
905+ journal = {American Economic Review},
906+ year = {2015},
907+ volume = {105},
908+ number = {2},
909+ pages = {564--608},
910+ doi = {10.1257/aer.20130456}
911+}
912+
913+@article{pasquariello2007,
914+ author = {Pasquariello, Paolo},
915+ title = {Imperfect Competition, Information Heterogeneity, and Financial Contagion},
916+ journal = {Review of Financial Studies},
917+ year = {2007},
918+ volume = {20},
919+ number = {2},
920+ pages = {391--426},
921+ doi = {10.1093/rfs/hhl010}
922+}
923+
924+@inproceedings{li2024econagent,
925+ author = {Li, Nian and Gao, Chen and Li, Mingyu and Li, Yong and Liao, Qingmin},
926+ title = {EconAgent: Large Language Model-Empowered Agents for Simulating Macroeconomic Activities},
927+ booktitle = {Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)},
928+ year = {2024},
929+ pages = {15523--15536},
930+ doi = {10.18653/v1/2024.acl-long.829},
931+ publisher = {Association for Computational Linguistics}
932+}
933+
934+@article{chen2023pnas,
935+ author = {Chen, Yiting and Liu, Tracy Xiao and Shan, You and Zhong, Songfa},
936+ title = {The emergence of economic rationality of GPT},
937+ journal = {Proceedings of the National Academy of Sciences},
938+ year = {2023},
939+ volume = {120},
940+ number = {51},
941+ doi = {10.1073/pnas.2316205120},
942+ pages = {e2316205120}
943+}
944+
945+@inproceedings{zhang2024finagent,
946+ author = {Zhang, Wentao and Zhao, Lingxuan and Xia, Haochong and Sun, Shuo and Sun, Jiaze and Qin, Molei and Li, Xinyi and Zhao, Yuqing and Zhao, Yilei and Cai, Xinyu and Zheng, Longtao and Wang, Xinrun and An, Bo},
947+ title = {A Multimodal Foundation Agent for Financial Trading: Tool-Augmented, Diversified, and Generalist},
948+ booktitle = {Proceedings of the 30th ACM SIGKDD Conference on Knowledge Discovery and Data Mining (KDD '24)},
949+ year = {2024},
950+ pages = {4314--4325},
951+ doi = {10.1145/3637528.3671801},
952+ publisher = {ACM}
953+}
954+
955+@article{calvano2020,
956+ author = {Calvano, Emilio and Calzolari, Giacomo and Denicolò, Vincenzo and Pastorello, Sergio},
957+ title = {Artificial Intelligence, Algorithmic Pricing, and Collusion},
958+ journal = {American Economic Review},
959+ year = {2020},
960+ volume = {110},
961+ number = {10},
962+ pages = {3267--3297},
963+ doi = {10.1257/aer.20190623}
964+}
965+
966+@article{klein2021,
967+ author = {Klein, Timo},
968+ title = {Autonomous algorithmic collusion: Q-learning under sequential pricing},
969+ journal = {The RAND Journal of Economics},
970+ year = {2021},
971+ volume = {52},
972+ number = {3},
973+ pages = {538--558},
974+ doi = {10.1111/1756-2171.12383}
975+}
976+
977+@article{assad2024,
978+ author = {Assad, Stephanie and Clark, Robert and Ershov, Daniel and Xu, Lei},
979+ title = {Algorithmic Pricing and Competition: Empirical Evidence from the German Retail Gasoline Market},
980+ journal = {Journal of Political Economy},
981+ year = {2024},
982+ volume = {132},
983+ number = {3},
984+ pages = {723--771},
985+ doi = {10.1086/726906}
986+}
987+
988+@article{brown2023,
989+ author = {Brown, Zach Y. and MacKay, Alexander},
990+ title = {Competition in Pricing Algorithms},
991+ journal = {American Economic Journal: Microeconomics},
992+ year = {2023},
993+ volume = {15},
994+ number = {2},
995+ pages = {109--156},
996+ doi = {10.1257/mic.20210158}
997+}
998+
999+@techreport{dou2025,
1000+ author = {Dou, Winston Wei and Goldstein, Itay and Ji, Yan},
1001+ title = {AI-Powered Trading, Algorithmic Collusion, and Price Efficiency},
1002+ year = {2025},
1003+ doi = {10.3386/w34054},
1004+ type = {NBER Working Paper},
1005+ number = {34054},
1006+ institution = {National Bureau of Economic Research}
1007+}
1008+
1009+@article{colliard2026,
1010+ author = {Colliard, Jean-Edouard and Foucault, Thierry and Lovo, Stefano},
1011+ title = {Algorithmic Pricing and Liquidity in Securities Markets},
1012+ journal = {The Review of Financial Studies},
1013+ year = {2026},
1014+ doi = {10.1093/rfs/hhag010},
1015+ note = {Forthcoming}
1016+}
1017+
1018+@article{kleinberg2021,
1019+ author = {Kleinberg, Jon and Raghavan, Manish},
1020+ title = {Algorithmic monoculture and social welfare},
1021+ journal = {Proceedings of the National Academy of Sciences},
1022+ year = {2021},
1023+ volume = {118},
1024+ number = {22},
1025+ doi = {10.1073/pnas.2018340118},
1026+ pages = {e2018340118}
1027+}
1028+
1029+@inproceedings{park2023,
1030+ author = {Park, Joon Sung and O'Brien, Joseph and Cai, Carrie Jun and Morris, Meredith Ringel and Liang, Percy and Bernstein, Michael S.},
1031+ title = {Generative Agents: Interactive Simulacra of Human Behavior},
1032+ booktitle = {Proceedings of the 36th Annual ACM Symposium on User Interface Software and Technology (UIST '23)},
1033+ year = {2023},
1034+ pages = {1--22},
1035+ doi = {10.1145/3586183.3606763},
1036+ publisher = {ACM}
1037+}
1038+
1039+@article{argyle2023,
1040+ author = {Argyle, Lisa P. and Busby, Ethan C. and Fulda, Nancy and Gubler, Joshua R. and Rytting, Christopher and Wingate, David},
1041+ title = {Out of One, Many: Using Language Models to Simulate Human Samples},
1042+ journal = {Political Analysis},
1043+ year = {2023},
1044+ volume = {31},
1045+ number = {3},
1046+ pages = {337--351},
1047+ doi = {10.1017/pan.2023.2}
1048+}
1049+
1050+@article{mei2024,
1051+ author = {Mei, Qiaozhu and Xie, Yutong and Yuan, Walter and Jackson, Matthew O.},
1052+ title = {A Turing test of whether AI chatbots are behaviorally similar to humans},
1053+ journal = {Proceedings of the National Academy of Sciences},
1054+ year = {2024},
1055+ volume = {121},
1056+ number = {9},
1057+ doi = {10.1073/pnas.2313925121},
1058+ pages = {e2313925121}
1059+}
1060+
1061+@article{akata2025,
1062+ author = {Akata, Elif and Schulz, Lion and Coda-Forno, Julian and Oh, Seong Joon and Bethge, Matthias and Schulz, Eric},
1063+ title = {Playing repeated games with large language models},
1064+ journal = {Nature Human Behaviour},
1065+ year = {2025},
1066+ volume = {9},
1067+ number = {7},
1068+ pages = {1380--1390},
1069+ doi = {10.1038/s41562-025-02172-y}
1070+}
1071+
1072+@article{chen2024hdsr,
1073+ author = {Chen, Lingjiao and Zaharia, Matei and Zou, James},
1074+ title = {How Is ChatGPT's Behavior Changing Over Time?},
1075+ journal = {Harvard Data Science Review},
1076+ year = {2024},
1077+ volume = {6},
1078+ number = {2},
1079+ doi = {10.1162/99608f92.5317da47}
1080+}
1081+
1082+@article{doshi2024,
1083+ author = {Doshi, Anil R. and Hauser, Oliver P.},
1084+ title = {Generative AI enhances individual creativity but reduces the collective diversity of novel content},
1085+ journal = {Science Advances},
1086+ year = {2024},
1087+ volume = {10},
1088+ number = {28},
1089+ doi = {10.1126/sciadv.adn5290},
1090+ pages = {eadn5290}
1091+}
1092+
1093+@article{park2024brm,
1094+ author = {Park, Peter S. and Schoenegger, Philipp and Zhu, Chongyang},
1095+ title = {Diminished diversity-of-thought in a standard large language model},
1096+ journal = {Behavior Research Methods},
1097+ year = {2024},
1098+ volume = {56},
1099+ number = {6},
1100+ pages = {5754--5770},
1101+ doi = {10.3758/s13428-023-02307-x}
1102+}
1103+
1104+@inproceedings{anderson2024,
1105+ author = {Anderson, Barrett R and Shah, Jash Hemant and Kreminski, Max},
1106+ title = {Homogenization Effects of Large Language Models on Human Creative Ideation},
1107+ booktitle = {Proceedings of the 16th Conference on Creativity and Cognition (C\&C '24)},
1108+ year = {2024},
1109+ pages = {413--425},
1110+ doi = {10.1145/3635636.3656204},
1111+ publisher = {ACM}
1112+}
1113+
1114+@article{schoenegger2024,
1115+ author = {Schoenegger, Philipp and Tuminauskaite, Indre and Park, Peter S. and Bastos, Rafael Valdece Sousa and Tetlock, Philip E.},
1116+ title = {Wisdom of the silicon crowd: LLM ensemble prediction capabilities rival human crowd accuracy},
1117+ journal = {Science Advances},
1118+ year = {2024},
1119+ volume = {10},
1120+ number = {45},
1121+ doi = {10.1126/sciadv.adp1528},
1122+ pages = {eadp1528}
1123+}
1124+
1125+@article{lussange2021,
1126+ author = {Lussange, Johann and Lazarevich, Ivan and Bourgeois-Gironde, Sacha and Palminteri, Stefano and Gutkin, Boris},
1127+ title = {Modelling Stock Markets by Multi-agent Reinforcement Learning},
1128+ journal = {Computational Economics},
1129+ year = {2021},
1130+ volume = {57},
1131+ number = {1},
1132+ pages = {113--147},
1133+ doi = {10.1007/s10614-020-10038-w}
1134+}
1135+
1136+@article{danielsson2022,
1137+ author = {Daníelsson, Jón and Macrae, Robert and Uthemann, Andreas},
1138+ title = {Artificial intelligence and systemic risk},
1139+ journal = {Journal of Banking & Finance},
1140+ year = {2022},
1141+ volume = {140},
1142+ pages = {106290},
1143+ doi = {10.1016/j.jbankfin.2021.106290}
1144+}
1145+
1146+@article{danielsson2025,
1147+ author = {Danielsson, Jon and Uthemann, Andreas},
1148+ title = {Artificial intelligence and financial crises},
1149+ journal = {Journal of Financial Stability},
1150+ year = {2025},
1151+ volume = {80},
1152+ pages = {101453},
1153+ doi = {10.1016/j.jfs.2025.101453}
1154+}
1155+
1156+@article{aldasoro2025,
1157+ author = {Aldasoro, I. and Gambacorta, L. and Korinek, A. and Shreeti, V. and Stein, M.},
1158+ title = {Intelligent financial system: How AI is transforming finance},
1159+ journal = {Journal of Financial Stability},
1160+ year = {2025},
1161+ volume = {81},
1162+ pages = {101472},
1163+ doi = {10.1016/j.jfs.2025.101472}
1164+}
1165+
1166+@techreport{hansen2025,
1167+ author = {Hansen, Anne Lundgaard and Lee, Seung Jung},
1168+ title = {Financial Stability Implications of Generative AI: Taming the Animal Spirits},
1169+ year = {2025},
1170+ number = {2025-090},
1171+ doi = {10.17016/FEDS.2025.090},
1172+ type = {Finance and Economics Discussion Series},
1173+ institution = {Board of Governors of the Federal Reserve System}
1174+}
1175+
1176+@techreport{gensler2020,
1177+ author = {Gensler, Gary and Bailey, Lily},
1178+ title = {Deep Learning and Financial Stability},
1179+ year = {2020},
1180+ doi = {10.2139/ssrn.3723132},
1181+ type = {SSRN Working Paper},
1182+ number = {3723132},
1183+ institution = {MIT Sloan School of Management}
1184+}
1185+
1186+@inproceedings{byrd2020,
1187+ author = {Byrd, David and Hybinette, Maria and Balch, Tucker Hybinette},
1188+ title = {ABIDES: Towards high-fidelity multi-agent market simulation},
1189+ booktitle = {Proceedings of the 2020 ACM SIGSIM Conference on Principles of Advanced Discrete Simulation (SIGSIM-PADS '20)},
1190+ year = {2020},
1191+ pages = {11--22},
1192+ doi = {10.1145/3384441.3395986},
1193+ publisher = {ACM}
1194+}
1195+
1196+@article{grossman1980,
1197+ author = {Grossman, Sanford J. and Stiglitz, Joseph E.},
1198+ title = {On the impossibility of informationally efficient markets},
1199+ journal = {American Economic Review},
1200+ year = {1980},
1201+ volume = {70},
1202+ number = {3},
1203+ pages = {393--408},
1204+ url = {https://www.jstor.org/stable/1805228}
1205+}
1206+
1207+@article{scharfstein1990,
1208+ author = {Scharfstein, David S. and Stein, Jeremy C.},
1209+ title = {Herd behavior and investment},
1210+ journal = {American Economic Review},
1211+ year = {1990},
1212+ volume = {80},
1213+ number = {3},
1214+ pages = {465--479},
1215+ url = {https://www.jstor.org/stable/2006678}
1216+}
1217+
1218+@techreport{horton2023,
1219+ author = {Horton, John J.},
1220+ title = {Large language models as simulated economic agents: What can we learn from Homo Silicus?},
1221+ type = {NBER Working Paper},
1222+ number = {31122},
1223+ institution = {National Bureau of Economic Research},
1224+ year = {2023},
1225+ doi = {10.3386/w31122}
1226+}
1227+
1228+@inproceedings{aher2023,
1229+ author = {Aher, Gati V. and Arriaga, Rosa I. and Kalai, Adam Tauman},
1230+ title = {Using large language models to simulate multiple humans and replicate human subject studies},
1231+ booktitle = {Proceedings of the 40th International Conference on Machine Learning},
1232+ series = {Proceedings of Machine Learning Research},
1233+ volume = {202},
1234+ pages = {337--371},
1235+ year = {2023},
1236+ publisher = {PMLR},
1237+ url = {https://proceedings.mlr.press/v202/aher23a.html}
1238+}
1239+
1240+@misc{bybee2023,
1241+ author = {Bybee, Leland},
1242+ title = {Surveying generative {AI}'s economic expectations},
1243+ year = {2023},
1244+ howpublished = {arXiv:2305.02823},
1245+ eprint = {2305.02823},
1246+ archivePrefix = {arXiv},
1247+ url = {https://arxiv.org/abs/2305.02823}
1248+}
1249+
1250+@misc{manning2024,
1251+ author = {Manning, Benjamin S. and Zhu, Kehang and Horton, John J.},
1252+ title = {Automated social science: Language models as scientist and subjects},
1253+ year = {2024},
1254+ howpublished = {arXiv:2404.11794},
1255+ eprint = {2404.11794},
1256+ archivePrefix = {arXiv},
1257+ url = {https://arxiv.org/abs/2404.11794}
1258+}
1259+
1260+@misc{lopezlira2023chatgpt,
1261+ author = {Lopez-Lira, Alejandro and Tang, Yuehua},
1262+ title = {Can {ChatGPT} forecast stock price movements? {Return} predictability and large language models},
1263+ year = {2023},
1264+ howpublished = {arXiv:2304.07619},
1265+ eprint = {2304.07619},
1266+ archivePrefix = {arXiv},
1267+ url = {https://arxiv.org/abs/2304.07619}
1268+}
1269+
1270+@misc{lopezlira2025trade,
1271+ author = {Lopez-Lira, Alejandro},
1272+ title = {Can large language models trade? {Testing} financial theories with {LLM} agents in market simulations},
1273+ year = {2025},
1274+ howpublished = {arXiv:2504.10789},
1275+ eprint = {2504.10789},
1276+ archivePrefix = {arXiv},
1277+ url = {https://arxiv.org/abs/2504.10789}
1278+}
1279+
1280+@misc{lopezlira2025memorization,
1281+ author = {Lopez-Lira, Alejandro and Tang, Yuehua and Zhu, Mingyin},
1282+ title = {The memorization problem: Can we trust {LLMs}' economic forecasts?},
1283+ year = {2025},
1284+ howpublished = {arXiv:2504.14765},
1285+ eprint = {2504.14765},
1286+ archivePrefix = {arXiv},
1287+ url = {https://arxiv.org/abs/2504.14765}
1288+}
1289+
1290+@misc{yu2023finmem,
1291+ author = {Yu, Yangyang and Li, Haohang and Chen, Zhi and Jiang, Yuechen and Li, Yang and Zhang, Denghui and Liu, Rong and Suchow, Jordan W. and Khashanah, Khaldoun},
1292+ title = {{FinMem}: A performance-enhanced {LLM} trading agent with layered memory and character design},
1293+ year = {2023},
1294+ howpublished = {arXiv:2311.13743},
1295+ eprint = {2311.13743},
1296+ archivePrefix = {arXiv},
1297+ url = {https://arxiv.org/abs/2311.13743}
1298+}
1299+
1300+@misc{li2023tradinggpt,
1301+ author = {Li, Yang and Yu, Yangyang and Li, Haohang and Chen, Zhi and Khashanah, Khaldoun},
1302+ title = {{TradingGPT}: Multi-agent system with layered memory and distinct characters for enhanced financial trading performance},
1303+ year = {2023},
1304+ howpublished = {arXiv:2309.03736},
1305+ eprint = {2309.03736},
1306+ archivePrefix = {arXiv},
1307+ url = {https://arxiv.org/abs/2309.03736}
1308+}
1309+
1310+@misc{zhang2024stockagent,
1311+ author = {Zhang, Chong and Liu, Xinyi and Zhang, Zhongmou and Jin, Mingyu and Li, Lingyao and Wang, Zhenting and Hua, Wenyue and Shu, Dong and Zhu, Suiyuan and Jin, Xiaobo and Li, Sujian and Du, Mengnan and Zhang, Yongfeng},
1312+ title = {When {AI} meets finance ({StockAgent}): Large language model-based stock trading in simulated real-world environments},
1313+ year = {2024},
1314+ howpublished = {arXiv:2407.18957},
1315+ eprint = {2407.18957},
1316+ archivePrefix = {arXiv},
1317+ url = {https://arxiv.org/abs/2407.18957}
1318+}
1319+
1320+@misc{gao2024asfm,
1321+ author = {Gao, Shen and Wen, Yuntao and Zhu, Minghang and Wei, Jianing and Cheng, Yuhan and Zhang, Qunzi and Shang, Shuo},
1322+ title = {Simulating financial market via large language model based agents},
1323+ year = {2024},
1324+ howpublished = {arXiv:2406.19966},
1325+ eprint = {2406.19966},
1326+ archivePrefix = {arXiv},
1327+ url = {https://arxiv.org/abs/2406.19966}
1328+}
1329+
1330+@misc{yang2025twinmarket,
1331+ author = {Yang, Yuzhe and Zhang, Yifei and Wu, Minghao and Zhang, Kaidi and Zhang, Yunmiao and Yu, Honghai and Hu, Yan and Wang, Benyou},
1332+ title = {{TwinMarket}: A scalable behavioral and social simulation for financial markets},
1333+ year = {2025},
1334+ howpublished = {arXiv:2502.01506; Advances in Neural Information Processing Systems 38 (NeurIPS 2025)},
1335+ eprint = {2502.01506},
1336+ archivePrefix = {arXiv},
1337+ url = {https://arxiv.org/abs/2502.01506}
1338+}
1339+
1340+@misc{henning2025,
1341+ author = {Henning, Thomas and Ojha, Siddhartha M. and Spoon, Ross and Han, Jiatong and Camerer, Colin F.},
1342+ title = {{LLM} agents do not replicate human market traders: Evidence from experimental finance},
1343+ year = {2025},
1344+ howpublished = {arXiv:2502.15800},
1345+ eprint = {2502.15800},
1346+ archivePrefix = {arXiv},
1347+ url = {https://arxiv.org/abs/2502.15800}
1348+}
1349+
1350+@misc{ouyang2026,
1351+ author = {Ouyang, Shumiao and Sui, Pengfei},
1352+ title = {Dissecting {AI} trading: Behavioral finance and market bubbles},
1353+ year = {2026},
1354+ howpublished = {arXiv:2604.18373},
1355+ eprint = {2604.18373},
1356+ archivePrefix = {arXiv},
1357+ url = {https://arxiv.org/abs/2604.18373}
1358+}
1359+
1360+@misc{saxena2026,
1361+ author = {Saxena, Maxime and Pangallo, Marco and Hommes, Cars and Caccioli, Fabio and {del Rio-Chanona}, R. Maria},
1362+ title = {Machine spirits: Speculation and adaptation of {LLM} agents in asset markets},
1363+ year = {2026},
1364+ howpublished = {arXiv:2604.18602},
1365+ eprint = {2604.18602},
1366+ archivePrefix = {arXiv},
1367+ url = {https://arxiv.org/abs/2604.18602}
1368+}
1369+
1370+@misc{struski2026,
1371+ author = {Struski, Pawel and Swistak, Jakub and Okulska, Inez and Biecek, Przemyslaw},
1372+ title = {Competitive market behavior of {LLMs}},
1373+ year = {2026},
1374+ howpublished = {arXiv:2609.02580},
1375+ eprint = {2609.02580},
1376+ archivePrefix = {arXiv},
1377+ url = {https://arxiv.org/abs/2609.02580}
1378+}
1379+
1380+@misc{chupilkin2026,
1381+ author = {Chupilkin, Maxim},
1382+ title = {Artificial institutions: How institutional design shapes {LLM} simulations},
1383+ year = {2026},
1384+ howpublished = {arXiv:2608.04020},
1385+ eprint = {2608.04020},
1386+ archivePrefix = {arXiv},
1387+ url = {https://arxiv.org/abs/2608.04020}
1388+}
1389+
1390+@misc{hashimoto2025,
1391+ author = {Hashimoto, Ryuji and Takayanagi, Takehiro and Suzuki, Masahiro and Izumi, Kiyoshi},
1392+ title = {Agent-based simulation of a financial market with large language models},
1393+ year = {2025},
1394+ howpublished = {arXiv:2510.12189},
1395+ eprint = {2510.12189},
1396+ archivePrefix = {arXiv},
1397+ url = {https://arxiv.org/abs/2510.12189}
1398+}
1399+
1400+@misc{papadakis2025,
1401+ author = {Papadakis, Charidimos and Filandrianos, Giorgos and Dimitriou, Angeliki and Lymperaiou, Maria and Thomas, Konstantinos and Stamou, Giorgos},
1402+ title = {{StockSim}: A dual-mode order-level simulator for evaluating multi-agent {LLMs} in financial markets},
1403+ year = {2025},
1404+ howpublished = {arXiv:2507.09255},
1405+ eprint = {2507.09255},
1406+ archivePrefix = {arXiv},
1407+ url = {https://arxiv.org/abs/2507.09255}
1408+}
1409+
1410+@misc{li2026switch,
1411+ author = {Li, Zeping and Wan, Guancheng and Chen, Keyang and Chen, Yu and Zhao, Yiwen and Torr, Philip and Ye, Guangnan and Yin, Zhenfei and Chai, Hongfeng},
1412+ title = {Behavioral consistency validation for {LLM} agents: An analysis of trading-style switching through stock-market simulation},
1413+ year = {2026},
1414+ howpublished = {arXiv:2602.07023},
1415+ eprint = {2602.07023},
1416+ archivePrefix = {arXiv},
1417+ url = {https://arxiv.org/abs/2602.07023}
1418+}
1419+
1420+@inproceedings{song2026,
1421+ author = {Song, Zichen},
1422+ title = {Information homogenization induces herding in retrieval-augmented {LLM} agent markets},
1423+ booktitle = {ICLR 2026 Workshop on Advances in Financial AI: Towards Agentic and Responsible Systems},
1424+ year = {2026},
1425+ url = {https://openreview.net/forum?id=8v4FskZp8I}
1426+}
1427+
1428+@misc{cho2025,
1429+ author = {Cho, Young-Min and Guntuku, Sharath Chandra and Ungar, Lyle},
1430+ title = {Herd behavior: Investigating peer influence in {LLM}-based multi-agent systems},
1431+ year = {2025},
1432+ howpublished = {arXiv:2505.21588},
1433+ eprint = {2505.21588},
1434+ archivePrefix = {arXiv},
1435+ url = {https://arxiv.org/abs/2505.21588}
1436+}
1437+
1438+@misc{agrawal2025,
1439+ author = {Agrawal, Kushal and Teo, Verona and Vazquez, Juan J. and Kunnavakkam, Sudarsh and Srikanth, Vishak and Liu, Andy},
1440+ title = {Evaluating {LLM} agent collusion in double auctions},
1441+ year = {2025},
1442+ howpublished = {arXiv:2507.01413},
1443+ eprint = {2507.01413},
1444+ archivePrefix = {arXiv},
1445+ url = {https://arxiv.org/abs/2507.01413}
1446+}
1447+
1448+@misc{fish2024,
1449+ author = {Fish, Sara and Gonczarowski, Yannai A. and Shorrer, Ran I.},
1450+ title = {Algorithmic collusion by large language models},
1451+ year = {2024},
1452+ howpublished = {arXiv:2404.00806},
1453+ eprint = {2404.00806},
1454+ archivePrefix = {arXiv},
1455+ url = {https://arxiv.org/abs/2404.00806}
1456+}
1457+
1458+@misc{bommasani2022,
1459+ author = {Bommasani, Rishi and Creel, Kathleen A. and Kumar, Ananya and Jurafsky, Dan and Liang, Percy},
1460+ title = {Picking on the same person: Does algorithmic monoculture lead to outcome homogenization?},
1461+ year = {2022},
1462+ howpublished = {arXiv:2211.13972; Advances in Neural Information Processing Systems 35 (NeurIPS 2022)},
1463+ eprint = {2211.13972},
1464+ archivePrefix = {arXiv},
1465+ url = {https://arxiv.org/abs/2211.13972}
1466+}
1467+
1468+@misc{bommasani2021,
1469+ author = {Bommasani, Rishi and Hudson, Drew A. and Adeli, Ehsan and Altman, Russ and Arora, Simran and {von Arx}, Sydney and Bernstein, Michael S. and Bohg, Jeannette and Bosselut, Antoine and Brunskill, Emma and Brynjolfsson, Erik and others},
1470+ title = {On the opportunities and risks of foundation models},
1471+ year = {2021},
1472+ howpublished = {arXiv:2108.07258},
1473+ eprint = {2108.07258},
1474+ archivePrefix = {arXiv},
1475+ url = {https://arxiv.org/abs/2108.07258}
1476+}
1477+
1478+@misc{gao2023s3,
1479+ author = {Gao, Chen and Lan, Xiaochong and Lu, Zhihong and Mao, Jinzhu and Piao, Jinghua and Wang, Huandong and Jin, Depeng and Li, Yong},
1480+ title = {S$^3$: Social-network simulation system with large language model-empowered agents},
1481+ year = {2023},
1482+ howpublished = {arXiv:2307.14984},
1483+ eprint = {2307.14984},
1484+ archivePrefix = {arXiv},
1485+ url = {https://arxiv.org/abs/2307.14984}
1486+}
1487+
1488+@misc{meng2026,
1489+ author = {Meng, Shuchen and Chen, Xupeng},
1490+ title = {Artificial intelligence and systemic risk: A unified model of performative prediction, algorithmic herding, and cognitive dependency in financial markets},
1491+ year = {2026},
1492+ howpublished = {arXiv:2604.03272},
1493+ eprint = {2604.03272},
1494+ archivePrefix = {arXiv},
1495+ url = {https://arxiv.org/abs/2604.03272}
1496+}
1497+
1498+@misc{immorlica2024,
1499+ author = {Immorlica, Nicole and Lucier, Brendan and Slivkins, Aleksandrs},
1500+ title = {Generative {AI} as economic agents},
1501+ year = {2024},
1502+ howpublished = {arXiv:2406.00477},
1503+ eprint = {2406.00477},
1504+ archivePrefix = {arXiv},
1505+ url = {https://arxiv.org/abs/2406.00477}
1506+}
1507+
1508+@misc{anthis2025,
1509+ author = {Anthis, Jacy Reese and Liu, Ryan and Richardson, Sean M. and Kozlowski, Austin C. and Koch, Bernard and Evans, James and Brynjolfsson, Erik and Bernstein, Michael},
1510+ title = {{LLM} social simulations are a promising research method},
1511+ year = {2025},
1512+ howpublished = {arXiv:2504.02234},
1513+ eprint = {2504.02234},
1514+ archivePrefix = {arXiv},
1515+ url = {https://arxiv.org/abs/2504.02234}
1516+}
1517+
1518+@techreport{fsb2024,
1519+ author = {{Financial Stability Board}},
1520+ title = {The financial stability implications of artificial intelligence},
1521+ institution = {Financial Stability Board},
1522+ address = {Basel},
1523+ year = {2024},
1524+ month = {November},
1525+ url = {https://www.fsb.org/uploads/P14112024.pdf}
1526+}
added paper/sections/theory.tex +470 −0
@@ -0,0 +1,470 @@
1+% paper/sections/theory.tex — UQO WP 27, "The Invisible Hand of Machines"
2+% Section: A stylized model of shared-model trading. Proofs in appendices/proofs.tex.
3+\section{A stylized model of shared-model trading}
4+\label{sec:theory}
5+
6+This section formalizes the mechanism that the simulator is designed to test: when many traders obtain
7+their trading signal from the \emph{same} model, their errors stop averaging out, prices become less
8+informative, orders become synchronized, and leverage-driven feedback becomes harder to contain. The
9+model is deliberately stylized. It is a one-period, partial-equilibrium pricing exercise in the spirit of
10+\citet{kyle1985} and \citet{hellwig1980}, with the correlated-signal structure of
11+\citet{fosterviswanathan1996} reinterpreted as the signature of a shared foundation model or prompt
12+family \citep{kleinbergraghavan2021}. Order submission is mechanical (linear in the signal) and the
13+response intensity is exogenous; the propositions below are therefore statements about how an
14+\emph{aggregation} mechanism transmits correlated errors into prices, not general-equilibrium results.
15+We state this limitation plainly wherever it matters.
16+
17+\subsection{Setup}
18+\label{sec:theory:setup}
19+
20+A single risky asset pays a liquidation value $v$. Normalizing the public prior (the public present
21+value of the simulator) to zero, $v\sim\mathcal N(0,\sigma_v^2)$. There are $N$ traders indexed by
22+$i=1,\dots,N$ and a set of $G\ge 1$ ``models'' indexed by $g=1,\dots,G$. A model is any shared
23+signal-generating device---a foundation model, a prompt family, a shared persona. Trader $i$ is attached
24+to model $g(i)\in\{1,\dots,G\}\cup\{\varnothing\}$, where $\varnothing$ denotes a private (idiosyncratic)
25+model. Let $n_g$ be the number of traders attached to model $g$, $\phi_g=n_g/N$ its share,
26+$\Phi=\sum_g\phi_g\le 1$ the total share of traders on \emph{some} shared model, and
27+\begin{equation}
28+ H \;=\; \sum_{g=1}^{G}\phi_g^2,\qquad M_{\mathrm{eff}}\;=\;1/H,
29+ \label{eq:herfindahl}
30+\end{equation}
31+the Herfindahl concentration of model use and the implied effective number of models.
32+
33+\begin{assumption}[A1: Gaussian primitives]
34+\label{ass:gauss}
35+Trader $i$ observes $s_i=v+\varepsilon_i$ with $\varepsilon_i=\eta_{g(i)}+u_i$. Model errors
36+$\eta_g\sim\mathcal N(0,\sigma_\eta^2)$ are i.i.d.\ across models; a trader on a private model draws
37+her own $\eta_i\sim\mathcal N(0,\sigma_\eta^2)$; idiosyncratic errors $u_i\sim\mathcal N(0,\sigma_u^2)$
38+are i.i.d.\ across traders; noise-trader demand is $z\sim\mathcal N(0,\sigma_z^2)$. All of
39+$v,\{\eta_g\},\{\eta_i\},\{u_i\},z$ are mutually independent.
40+\end{assumption}
41+
42+Under A1 every trader's signal has the same precision---$\operatorname{Var}(\varepsilon_i)=\sigma_\eta^2+\sigma_u^2$
43+regardless of $g(i)$---so that the shares $(\phi_g)$ change only the \emph{correlation} of errors across
44+traders, never the quality of any individual signal. This is the ``holding information quality fixed''
45+clause of hypothesis H1.
46+
47+\begin{assumption}[A2: linear order submission]
48+\label{ass:linear}
49+Trader $i$ submits the market order $x_i=\beta s_i$ with $\beta>0$ a fixed, common response intensity.
50+\end{assumption}
51+
52+A2 mirrors rule-like policies: an LLM agent given a snapshot returns an order whose size scales with
53+the strength of the signal it extracts, and it does not condition on the price impact of other agents
54+using the same model. The intensity $\beta$ is a primitive here, not an equilibrium object; a
55+CARA/price-taking version in which $\beta$ depends on $\lambda$ and on risk aversion
56+\citep{hellwig1980,grossmanstiglitz1980} is possible but is not needed for the comparative statics
57+we test, and we do not pursue it.
58+
59+\begin{assumption}[A3: competitive risk-neutral market maker]
60+\label{ass:mm}
61+A competitive, risk-neutral market maker observes the aggregate order flow $Y=\sum_{i=1}^N x_i+z$ and
62+sets $P=\mathbb E[v\mid Y]$.
63+\end{assumption}
64+
65+\begin{assumption}[A4: partial equilibrium]
66+\label{ass:pe}
67+$\beta$, $\sigma_z^2$ and the shares $(\phi_g)$ are exogenous; traders do not choose whether to adopt
68+a shared model, and the market maker does not re-optimize $\lambda$ against the feedback demands
69+introduced in Sections~\ref{sec:theory:feedback}--\ref{sec:theory:spiral}.
70+\end{assumption}
71+
72+Under A1--A3 the projection theorem gives the linear pricing rule
73+\begin{equation}
74+ P=\lambda Y,\qquad
75+ \lambda=\frac{\operatorname{Cov}(v,Y)}{\operatorname{Var}(Y)}
76+ =\frac{\beta N\sigma_v^2}{\beta^2N^2\left(\sigma_v^2+\Omega\right)},
77+ \qquad
78+ \Omega\equiv\sigma_\eta^2\Big[H+\frac{1-\Phi}{N}\Big]+\frac{\sigma_u^2}{N}+\frac{\sigma_z^2}{\beta^2N^2}.
79+ \label{eq:lambda}
80+\end{equation}
81+The quantity $\Omega$ is the variance of the noise in the \emph{normalized} order flow
82+$\bar Y\equiv Y/(\beta N)=v+\bar\varepsilon+z/(\beta N)$, where $\bar\varepsilon=N^{-1}\sum_i\varepsilon_i$;
83+every result below is a statement about $\Omega$. The three objects we track are price informativeness,
84+the pricing error, and order-flow co-movement:
85+\begin{equation}
86+ \tau\equiv 1-\frac{\operatorname{Var}(v\mid Y)}{\sigma_v^2}=\frac{\sigma_v^2}{\sigma_v^2+\Omega},
87+ \qquad
88+ \operatorname{Var}(P-v)=\sigma_v^2(1-\tau),
89+ \qquad
90+ \rho_{ij}\equiv\operatorname{Corr}(x_i,x_j),
91+ \label{eq:objects}
92+\end{equation}
93+and the pricing rule can be written compactly as $\lambda=\tau/(\beta N)$, so that
94+$P=\tau\,\bar Y$: the price is the normalized flow shrunk toward the prior by the informativeness
95+coefficient. The identity $\operatorname{Var}(P-v)=\sigma_v^2(1-\tau)$ (proved in
96+Appendix~\ref{app:proofs}) means that, within the one-period model, informativeness and the mean
97+squared pricing error are two readings of the same number; the simulator's dynamic statistics
98+separate them.
99+
100+\subsection{Informativeness and penetration of a single shared model}
101+\label{sec:theory:info}
102+
103+Consider first a single shared model ($G=1$) used by a share $\phi=n/N$ of traders, all others being
104+on private models. Then $H=\phi^2$, $\Phi=\phi$, and
105+\begin{equation}
106+ \Omega(\phi)=\sigma_\eta^2\Big[\phi^2+\frac{1-\phi}{N}\Big]+\frac{\sigma_u^2}{N}+\frac{\sigma_z^2}{\beta^2N^2},
107+ \qquad
108+ \tau(\phi)=\frac{\sigma_v^2}{\sigma_v^2+\Omega(\phi)}.
109+ \label{eq:tau_phi}
110+\end{equation}
111+
112+\begin{proposition}[Informativeness versus penetration]
113+\label{prop:info}
114+Under A1--A4 with a single shared model of share $\phi$:
115+\begin{enumerate}
116+\item[(i)] For every $N>1$, $\tau$ is strictly decreasing in $\phi$ over the admissible grid
117+ $\phi\in\{1/N,2/N,\dots,1\}$; in the continuous relaxation, $\partial\tau/\partial\phi<0$ for all
118+ $\phi>1/(2N)$. The values $\phi=0$ and $\phi=1/N$ coincide, since a model used by a single trader is a
119+ private model. For $N=1$ the share has no effect.
120+\item[(ii)] For every $N$ and every $\phi>0$,
121+ $\tau(\phi)\le\tau_\infty(\phi)\equiv\sigma_v^2/(\sigma_v^2+\phi^2\sigma_\eta^2)<1$, and
122+ $\tau(\phi)\to\tau_\infty(\phi)$ as $N\to\infty$ with $\phi$ fixed: the common error never averages
123+ out.
124+\item[(iii)] If $\phi=0$ and $\sigma_z^2/N^2\to 0$ (in particular if $\sigma_z^2$ is fixed), then
125+ $\tau\to 1$ as $N\to\infty$.
126+\end{enumerate}
127+\end{proposition}
128+
129+\noindent\emph{Intuition.} The market maker learns from the average signal. Private errors are
130+averaged over $N$ draws and vanish at rate $1/N$; the shared error $\eta$ is a single draw that enters
131+the average with weight $\phi$ and therefore survives with variance $\phi^2\sigma_\eta^2$ no matter how
132+many traders there are. Penetration converts a diversifiable error into a non-diversifiable one. Note
133+that (ii) also implies that the \emph{marginal} value of adding traders falls with $\phi$: with a
134+shared model, the market behaves as if it had at most $M_{\mathrm{eff}}=1/\phi^2$ independent signals.
135+
136+\subsection{Pricing error, order-flow correlation and synchrony}
137+\label{sec:theory:corr}
138+
139+Let $\sigma_s^2\equiv\sigma_v^2+\sigma_\eta^2+\sigma_u^2$ be the variance of a signal, and define the two
140+possible pairwise correlations,
141+\begin{equation}
142+ \rho_1=\frac{\sigma_v^2+\sigma_\eta^2}{\sigma_s^2}\quad(\text{same model}),\qquad
143+ \rho_0=\frac{\sigma_v^2}{\sigma_s^2}\quad(\text{different or private models}),\qquad
144+ \rho_1-\rho_0=\frac{\sigma_\eta^2}{\sigma_s^2}.
145+ \label{eq:rho01}
146+\end{equation}
147+Let $\bar\rho$ denote the population-average pairwise correlation over the $N(N-1)$ ordered pairs
148+$i\neq j$, and let the \emph{synchrony index} of a pair be the probability that the two traders submit
149+same-signed orders, $S_{ij}=\Pr(x_ix_j>0)$, with population average $\bar S$.
150+
151+\begin{proposition}[Pricing error, correlation and synchrony]
152+\label{prop:corr}
153+Under A1--A4 with a single shared model of share $\phi=n/N$:
154+\begin{enumerate}
155+\item[(i)] $\operatorname{Var}(P-v)=\sigma_v^2\,\Omega(\phi)/(\sigma_v^2+\Omega(\phi))$ is increasing in
156+ $\phi$ on the admissible grid (and for all real $\phi>1/(2N)$), and is bounded below by
157+ $\sigma_v^2\phi^2\sigma_\eta^2/(\sigma_v^2+\phi^2\sigma_\eta^2)$ uniformly in $N$.
158+\item[(ii)] $\operatorname{Corr}(x_i,x_j)=\rho_1$ if $g(i)=g(j)\neq\varnothing$ and $\rho_0$ otherwise;
159+ $\rho_1>\rho_0$ if and only if $\sigma_\eta^2>0$.
160+\item[(iii)] The average pairwise correlation is
161+ \begin{equation}
162+ \bar\rho(\phi)=\rho_0+w(\phi)\,\frac{\sigma_\eta^2}{\sigma_s^2},\qquad
163+ w(\phi)=\frac{n(n-1)}{N(N-1)}=\frac{\phi(N\phi-1)}{N-1},
164+ \label{eq:rhobar}
165+ \end{equation}
166+ which is convex in $\phi$, increasing for $\phi>1/(2N)$, and converges to
167+ $\rho_0+\phi^2\sigma_\eta^2/\sigma_s^2$ as $N\to\infty$.
168+\item[(iv)] For a pair with correlation $\rho$, $S(\rho)=\tfrac12+\arcsin(\rho)/\pi$, which is strictly
169+ increasing in $\rho$. The population synchrony index is
170+ \begin{equation}
171+ \bar S(\phi)=\tfrac12+\frac{1}{\pi}\Big[(1-w(\phi))\arcsin\rho_0+w(\phi)\arcsin\rho_1\Big],
172+ \label{eq:sync}
173+ \end{equation}
174+ which is a strictly increasing function of $\bar\rho$ whenever $\sigma_\eta^2>0$, and satisfies the
175+ Jensen bound $\bar S\ge\tfrac12+\arcsin(\bar\rho)/\pi$ with equality if and only if $\sigma_\eta^2=0$.
176+\end{enumerate}
177+If $\sigma_\eta^2=0$, none of $\tau$, $\operatorname{Var}(P-v)$, $\bar\rho$ or $\bar S$ depends on $\phi$.
178+\end{proposition}
179+
180+\noindent\emph{Intuition.} Two traders on the same model agree about the world more often than the
181+fundamentals justify, because they share the model's mistake. The share of such pairs is quadratic in
182+$\phi$ (it is the probability that two random traders both use the model), which is why co-movement
183+and synchrony accelerate with penetration. The last sentence is the natural placebo: a shared model
184+that adds no error of its own ($\sigma_\eta^2=0$) is just a precise private signal and cannot generate
185+any of the effects.
186+
187+\subsection{Diversity as a stabilizer}
188+\label{sec:theory:diversity}
189+
190+\begin{proposition}[Diversity]
191+\label{prop:diversity}
192+Under A1--A4 with $G$ shared models of shares $(\phi_1,\dots,\phi_G)$ and total share $\Phi$:
193+\begin{enumerate}
194+\item[(i)] Holding $\Phi$ fixed, $\tau$ and $\operatorname{Var}(P-v)$ depend on the shares only through
195+ $H=\sum_g\phi_g^2$, with $\partial\tau/\partial H<0$. When $\Phi=1$,
196+ $\Omega=\sigma_\eta^2/M_{\mathrm{eff}}+\sigma_u^2/N+\sigma_z^2/(\beta^2N^2)$: model error averages out at
197+ rate $1/M_{\mathrm{eff}}$ while private error averages out at rate $1/N$.
198+\item[(ii)] $\Phi^2/G\le H\le\Phi^2$. The upper bound is attained if and only if a single model carries
199+ the whole share $\Phi$; the lower bound if and only if the $G$ models have equal shares
200+ $\phi_g=\Phi/G$ (in the integer version, if and only if the $n_g$ differ by at most one).
201+\item[(iii)] Consequently, for any population with total shared-model share $\Phi$,
202+ $\tau_{\text{single}}\le\tau_{\text{mixed}}\le\tau_{\text{uniform}}$: a mixed-provider or
203+ diverse-prompt population is weakly more informative than a single-model population of the same size,
204+ strictly so whenever at least two models have positive share and $\sigma_\eta^2>0$.
205+\item[(iv)] The average correlation generalizes to $\bar\rho=\rho_0+w\,\sigma_\eta^2/\sigma_s^2$ with
206+ $w=(NH-\Phi)/(N-1)$, so that, for fixed $\Phi$, $\bar\rho$ and $\bar S$ are also increasing in $H$.
207+\end{enumerate}
208+\end{proposition}
209+
210+\noindent\emph{Intuition.} With several models, the common errors are themselves averaged over models.
211+The Herfindahl index is the exact sufficient statistic because the variance of a weighted average of
212+independent model errors is $\sigma_\eta^2\sum_g\phi_g^2$. Splitting a given LLM population evenly across
213+$G$ providers (or prompt families with independent errors) reduces the undiversifiable noise from
214+$\Phi^2\sigma_\eta^2$ to $\Phi^2\sigma_\eta^2/G$. Note that the recovery is partial unless $G$ grows with
215+$N$: $M_{\mathrm{eff}}\le N$ and diversity across a handful of providers cannot restore the
216+$1/N$ rate (hypothesis H4). In this stylized model the benefit of diversity rests entirely on the
217+independence of $\eta_g$ across models; models trained on similar data or distilled from each other
218+would have correlated $\eta_g$ and a correspondingly larger effective $H$.
219+
220+\subsection{Feedback amplification}
221+\label{sec:theory:feedback}
222+
223+We now add a second date and trend demand in the spirit of \citet{delong1990}. Let $P_0=0$ be the
224+public value before the trading round and $Y_1=\beta\sum_i s_i+z_1$ the fundamental flow of
225+Section~\ref{sec:theory:setup}. Trend followers submit the demand $\gamma(P_1-P_0)$, $\gamma\ge 0$.
226+
227+\begin{assumption}[A5: within-round feedback with a fixed pricing schedule]
228+\label{ass:feedback}
229+Trend demand responds to the price change of the current round, and the market maker applies the
230+schedule $\lambda$ of \eqref{eq:lambda} to the total flow $Y_1+\gamma(P_1-P_0)$ without re-estimating
231+it.
232+\end{assumption}
233+
234+A5 is the model counterpart of the simulator's timing: fundamental and LLM orders arrive in staggered
235+batches over a decision interval, while trend followers react every tick to the mid-price history, so
236+within a decision interval the trend response is effectively contemporaneous. Its second clause is a
237+restriction, not a result: a Bayesian market maker who knew that the extra flow is $\gamma\Delta P$
238+would filter it out (the total flow is then a deterministic multiple of $Y_1$), and there would be no
239+amplification. The simulator's market makers quote a mechanical inventory-skewed schedule and do not
240+filter, which is what A5 captures.
241+
242+\begin{proposition}[Feedback amplification]
243+\label{prop:feedback}
244+Under A1--A5, write $\Delta P=P_1-P_0$ and $\kappa\equiv 1/(1-\lambda\gamma)$.
245+\begin{enumerate}
246+\item[(i)] If $\lambda\gamma<1$ the round has the unique fixed point $\Delta P=\kappa\lambda Y_1$, which
247+ is the limit of the iterated response $\Delta P^{(k+1)}=\lambda\big(Y_1+\gamma\Delta P^{(k)}\big)$
248+ from any starting point. The variance of the price change is amplified by $\kappa^2=1/(1-\lambda\gamma)^2$
249+ relative to the no-feedback benchmark.
250+\item[(ii)] If $\lambda\gamma\ge 1$ the iterated response diverges, $|\Delta P^{(k)}|\to\infty$, from
251+ every starting point except the algebraic solution. For $\lambda\gamma=1$ no solution exists when
252+ $Y_1\neq0$; for $\lambda\gamma>1$ the algebraic solution $\Delta P=\kappa\lambda Y_1$ has the sign
253+ opposite to the flow and is repelling, so no adjustment process reaches it.
254+\item[(iii)] Amplification leaves $\tau$ unchanged (the price remains a deterministic multiple of
255+ $Y_1$) but raises the pricing error to
256+ $\operatorname{Var}(P_1-v)=\sigma_v^2\big[(1-\tau)+\tau(\kappa-1)^2\big]$, increasing in $\gamma$ on
257+ $[0,1/\lambda)$.
258+\end{enumerate}
259+\end{proposition}
260+
261+\begin{assumption}[A6: return-loading of LLM policies]
262+\label{ass:loading}
263+An LLM trader on model $g$ submits $x_i=\beta s_i+\kappa_g\,\Delta P$ with $\kappa_g\ge 0$ the
264+model's loading on the current price change; classical trend followers contribute an aggregate loading
265+$\gamma_T\ge0$.
266+\end{assumption}
267+
268+\begin{corollary}[Shared models raise the effective feedback coefficient]
269+\label{cor:llmtrend}
270+Under A1--A6 the effective feedback coefficient is $\gamma=\gamma_T+N\sum_g\phi_g\kappa_g$. With a single
271+shared model of share $\phi$ and loading $\kappa>0$, $\gamma(\phi)=\gamma_T+N\phi\kappa$ is increasing in
272+$\phi$, the amplification factor $\kappa^2(\phi)$ is increasing in $\phi$, and the stable regime is lost
273+at the critical share $\phi^\ast=(1/\lambda-\gamma_T)/(N\kappa)$. If $\kappa=0$ the share has no
274+effect on amplification. If the loadings $\kappa_g$ differ in sign across models (some models are
275+contrarian), the aggregate loading is the share-weighted average and can be smaller than that of any
276+single model: diversity dampens feedback, concentration does not.
277+\end{corollary}
278+
279+\noindent\emph{Intuition.} A price rise induced by informed flow triggers trend demand, which under a
280+fixed pricing schedule moves the price again, and so on; the geometric sum converges only if each
281+round's response is smaller than the last. LLM policies that mention momentum, ``recent trend'' or
282+``follow the tape'' in their rationale behave like trend followers with a loading $\kappa$ that the
283+simulator measures by regressing LLM orders on lagged returns. Whether $\kappa$ is positive is an
284+empirical matter; the corollary says what follows if it is. The variance amplification is
285+identical to the one obtained by DeLong et al.\ with rational speculators front-running feedback
286+traders, but the source here is mechanical.
287+
288+\subsection{Liquidity spiral}
289+\label{sec:theory:spiral}
290+
291+Finally, we add leverage, following the margin-spiral logic of \citet{brunnermeierpedersen2009}. A
292+cohort $L$ of $N_L$ leveraged traders holds the aggregate position $X$ (in lots) financed with equity
293+$W$ under a maintenance requirement of $m>0$ units of equity per lot held.\footnote{If the simulator's
294+maintenance margin rate is $\hat m$ and the pre-shock price is $\bar P$, then $m=\hat m\bar P$ with the
295+collateral valued at the pre-shock price; this linearization is exact to first order in
296+$|\Delta P|/\bar P$ and is the only approximation in the proposition.} Let $b\equiv W-mX\ge0$ be the
297+initial margin buffer. An exogenous price shock $\Delta P_0<0$ hits the cohort's side of the market;
298+after a total price change $\Delta P$ the cohort's equity is $W+X\Delta P$, so it must liquidate
299+\begin{equation}
300+ F(\Delta P)=\max\Big\{0,\;X-\frac{W+X\Delta P}{m}\Big\}=\max\Big\{0,\;\frac{-X\Delta P-b}{m}\Big\}
301+ \label{eq:forced}
302+\end{equation}
303+lots. Under A5 the market maker prices the forced sales as order flow, so the total price change solves
304+$\Delta P=\Delta P_0-\lambda F(\Delta P)$. Define the spiral coefficient $a\equiv\lambda X/m$.
305+
306+\begin{proposition}[Liquidity spiral]
307+\label{prop:spiral}
308+Under A1--A5 and \eqref{eq:forced}:
309+\begin{enumerate}
310+\item[(i)] If $\Delta P_0\ge -b/X$ the shock is absorbed by the buffer: $F=0$ and $\Delta P=\Delta P_0$
311+ is a fixed point at which the cascade stops at once. (When $a>1$ and $b>0$ a second, self-fulfilling
312+ fixed point with $F>0$ also exists; it is not reached by the cascade and we select the cascade limit
313+ throughout.)
314+\item[(ii)] If $\Delta P_0<-b/X$ and $a<1$, the unique fixed point is
315+ \begin{equation}
316+ \Delta P^\ast=\frac{\Delta P_0+\lambda b/m}{1-a},\qquad\text{so that with } b=0:\quad
317+ \Delta P^\ast=\frac{\Delta P_0}{1-\lambda X/m},
318+ \label{eq:spiral_fp}
319+ \end{equation}
320+ with amplification multiplier $1/(1-\lambda X/m)>1$. The fixed point is the limit of the
321+ round-by-round cascade in which each round's forced sales respond to the previous round's price.
322+\item[(iii)] If $\Delta P_0<-b/X$ and $a\ge 1$, no finite fixed point exists and the cascade decreases
323+ without bound: in a market with a price band, the price reaches the band.
324+\item[(iv)] Let $X=\big|\sum_{i\in L}x_i\big|$ be the absolute net position of the cohort and $\bar\rho_L$
325+ its average pairwise order correlation. Then $X$ is the absolute value of a centered Gaussian with
326+ standard deviation $\sigma_X=\beta\sigma_s\sqrt{N_L[1+(N_L-1)\bar\rho_L]}$, so that
327+ $\mathbb E[X]=\sigma_X\sqrt{2/\pi}$ and the spiral probability $\Pr(\lambda X/m\ge1)=2\bar\Phi(m/(\lambda\sigma_X))$
328+ are both strictly increasing in $\bar\rho_L$, hence (Proposition~\ref{prop:corr}) in the shared-model
329+ share of the cohort. (Here $\bar\Phi$ is the standard normal survival function.)
330+\end{enumerate}
331+\end{proposition}
332+
333+\noindent\emph{Intuition.} The forced sale of one lot per $m/X$ of price decline moves the price by
334+$\lambda$ per lot; when $\lambda X/m\ge1$ each round of liquidation generates at least as large a
335+decline as the one that triggered it. A shared model does not change the expected position of any
336+single leveraged trader, but it makes positions line up on the same side, which fattens the
337+distribution of the cohort's net exposure; the spiral threshold $m/\lambda$ is crossed more often. The
338+net position is a conservative base for forced sales---the gross exposure on the shocked side is
339+weakly larger---so (iv) understates the mechanism. Since by \eqref{eq:lambda} $\lambda$ itself is
340+decreasing in $\Omega$, there is a countervailing channel (a less informative market is also a less
341+price-sensitive one); in the simulator, however, $\lambda$ is set by the mechanical market makers' depth,
342+so we treat it as a parameter.
343+
344+\subsection{Safeguards}
345+\label{sec:theory:safeguards}
346+
347+Two safeguards from the simulator have direct counterparts. A \emph{position limit} caps the cohort's
348+exposure at $L$ lots (a per-trader cap $\ell$ with $N_L\ell=L$). A \emph{trading halt} of length $h\ge1$
349+rounds is triggered when the price change reaches a threshold $\theta>0$; during the halt no forced
350+sales execute. For the halt we need a reduced-form statement of what changes while trading is stopped.
351+
352+\begin{assumption}[A7: halt effectiveness]
353+\label{ass:halt}
354+After a halt of length $h$, the cascade resumes with spiral coefficient $\psi(h)\,a$, where
355+$\psi:\{0,1,2,\dots\}\to(0,1]$ is nonincreasing with $\psi(0)=1$.
356+\end{assumption}
357+
358+A7 summarizes two mechanisms, margin top-ups (fewer lots must be sold per unit of price decline) and
359+the arrival of patient liquidity (smaller impact per lot sold), without modelling either. It is an
360+assumption about the halt, not a result; the case $\psi(h)=1$ is a halt that changes nothing but the
361+clock.
362+
363+\begin{proposition}[Safeguards]
364+\label{prop:safeguards}
365+Under the assumptions of Proposition~\ref{prop:spiral}:
366+\begin{enumerate}
367+\item[(i)] (Position limit.) If $L<m/\lambda$, then for every shared-model share and every realization
368+ of signals, $\lambda X/m\le\lambda L/m<1$: the spiral region is empty and the multiplier is bounded by
369+ $1/(1-\lambda L/m)$. The cost is informational: if the limit binds on informed orders, each capped
370+ order $\tilde x_i=\min\{\max\{\beta s_i,-\ell\},\ell\}$ has loading
371+ $\operatorname{Cov}(v,\tilde x_i)=\Pr(|\beta s_i|<\ell)\operatorname{Cov}(v,x_i)<\operatorname{Cov}(v,x_i)$ on the
372+ fundamental.
373+\item[(ii)] (Halt: depth.) Under A7, if the halt is triggered by the shock itself ($|\Delta P_0|\ge\theta$)
374+ and $\psi(h)a<1$, the post-halt cascade converges to
375+ $\Delta P^\ast_h=(\Delta P_0+\lambda b/m)/(1-\psi(h)a)$, which is finite even when $a\ge1$ and satisfies
376+ $|\Delta P^\ast_h|\le|\Delta P^\ast|$ when $a<1$, strictly if $\psi(h)<1$.
377+\item[(iii)] (Halt: time-to-absorb.) Let $T_\epsilon$ be the first round at which the price is within
378+ $\epsilon$ of its terminal value. Without a halt and $a<1$,
379+ $T_\epsilon(a)=\max\{0,\lceil\log(\epsilon(1-a)/|\Delta P_0|)/\log a\rceil-1\}$ (for $b=0$; for $b>0$
380+ replace $|\Delta P_0|$ by $|\Delta P_0|-b/X$); with the halt,
381+ $T^{h}_\epsilon=h+T_\epsilon(\psi(h)a)$. Hence $T^h_\epsilon\ge h$: no information---neither the shock
382+ itself nor public news arriving during the halt---is incorporated before $h$ rounds have elapsed,
383+ and if $\psi(h)=1$ the halt is pure delay, $T^h_\epsilon=T_\epsilon(a)+h$.
384+\end{enumerate}
385+\end{proposition}
386+
387+\noindent\emph{Intuition.} A position limit works by construction: it bounds the base of forced sales
388+and therefore the slope of the feedback loop, at the cost of truncating the informed demand that makes
389+prices informative. A halt is more ambiguous. It reduces the depth of the spiral only through what
390+changes during the halt (A7), and it always costs at least its own length in delayed price discovery;
391+whether the trade-off is favourable is the empirical question H6 asks of the simulator.
392+
393+\subsection{Mapping to the simulator}
394+\label{sec:mapping}
395+
396+Table~\ref{tab:mapping} maps each theoretical primitive to the simulator parameter that manipulates or
397+measures it. Two points deserve emphasis. First, $\phi$ is \emph{not} the LLM share: it is the share of
398+traders on one and the same model, i.e.\ \texttt{llm\_share} times the share of LLM slots on a given
399+provider--prompt combination (\texttt{provider\_mix}, \texttt{prompt\_set}). A population with
400+\texttt{llm\_share} $=0.5$ split evenly across two providers with diverse prompts has a small $H$ and
401+should behave, in the model, like a population of private models. Second, $\beta$ and $\lambda$ are
402+measured, not set: the LLM order aggressiveness is the slope of submitted quantity on the private signal
403+(or on the public-value gap for uninformed LLM agents), and $\lambda$ is the Kyle lambda estimated by
404+regressing mid-price changes on signed order flow.
405+
406+\begin{table}[htbp]
407+\centering
408+\caption{Theoretical primitives and their simulator counterparts.}
409+\label{tab:mapping}
410+\small
411+\begin{tabular}{p{0.26\linewidth}p{0.68\linewidth}}
412+\hline
413+Primitive & Simulator parameter or statistic \\
414+\hline
415+$\phi$, $(\phi_g)$, $H$ & \texttt{population.llm\_share} $\times$ share of LLM slots on the same (provider, model, prompt family): \texttt{llm.provider\_mix}, \texttt{llm.prompt\_set} (\texttt{single}/\texttt{diverse}); $H$ computed from the realized assignment \\
416+$\sigma_\eta^2$ & shared-model error: variance of the common component of LLM decisions given the snapshot; manipulated by prompt diversity, \texttt{llm.memory}, \texttt{llm.channel}, estimated from the decision library (same-snapshot replays) \\
417+$\sigma_u^2$ & private-signal noise $\sigma^2/\texttt{precision}$ (\texttt{population.precision}, \texttt{llm.precision}) and \texttt{llm.informed\_share} \\
418+$\sigma_v^2$ & dividend-innovation variance $\sigma^2$ scaled by $\rho/(1+r-\rho)$ (\texttt{fundamentals.sigma}, \texttt{rho}, \texttt{r\_period}) \\
419+$\beta$ & LLM order aggressiveness: measured slope of order size on the signal (\texttt{llm.max\_order\_qty} caps it) \\
420+$\lambda$ & estimated Kyle lambda (mid change on signed volume over \texttt{div\_interval} windows); depth set by market-maker slots (5\,\%) \\
421+$\gamma$, $\kappa_g$ & \texttt{population.mix[trend]}, \texttt{mix[leveraged]}; LLM return loading estimated from decisions on lagged returns \\
422+$m$, $W$, $X$ & \texttt{population.maintenance\_margin}, \texttt{leverage\_cap}, endowments; $X$ = realized net position of leveraged slots \\
423+$L$, $\ell$ & position limit (per-agent order/position cap; \texttt{allow\_short}, \texttt{max\_short\_lots}) \\
424+$\theta$, $h$ & \texttt{exchange.halt\_threshold}, \texttt{halt\_window}, \texttt{halt\_duration} (circuit breaker) \\
425+$z$, $\sigma_z^2$ & noise-trader slots (\texttt{mix[noise]}, activity) \\
426+$N$ & \texttt{population.n\_traders} ($100$, $1{,}000$, $10{,}000$) \\
427+$\tau$ & $R^2$ of the latent value $V_t$ on the mid price (post-warmup) or $1-\operatorname{Var}(V_t-P_t)/\operatorname{Var}(V_t)$ \\
428+$\operatorname{Var}(P-v)$ & mean squared pricing error $(P_t-V_t)^2$ \\
429+$\bar\rho$, $\bar S$ & average pairwise correlation of signed order flow across agents; share of agent pairs with same-signed orders within a decision interval \\
430+$\Delta P^\ast$, $T_\epsilon$ & crash depth after a shock event (\texttt{shocks}); time-to-absorb = ticks until $|P_t-V^{\mathrm{pub}}_t|$ re-enters a tolerance band \\
431+\hline
432+\end{tabular}
433+\end{table}
434+
435+\subsection{Testable predictions and what would falsify them}
436+\label{sec:predictions}
437+
438+Table~\ref{tab:predictions} lists the hypotheses of the pre-analysis plan together with the
439+proposition that generates each, the predicted sign and the simulator statistic that tests it. Each
440+line also states what would count against the model. We stress once more that these are predictions of
441+a stylized, partial-equilibrium aggregation model; the simulator is not an implementation of the model
442+but an environment in which the same mechanism should, if it is real, leave the same signature.
443+
444+\begin{table}[htbp]
445+\centering
446+\caption{Hypotheses, generating propositions, predicted signs and tests.}
447+\label{tab:predictions}
448+\small
449+\begin{tabular}{p{0.04\linewidth}p{0.33\linewidth}p{0.13\linewidth}p{0.38\linewidth}}
450+\hline
451+ & Prediction (source) & Sign & Test statistic; falsifying pattern \\
452+\hline
453+H1 & Informativeness falls in the single-model share $\phi$ at fixed signal quality and rises in $M_{\mathrm{eff}}$ (Props.~\ref{prop:info}, \ref{prop:diversity}) & $\partial\tau/\partial\phi<0$; $\partial\tau/\partial M_{\mathrm{eff}}>0$ & slope of $\tau$ on \texttt{llm\_share} within the single-provider/single-prompt arm, matched pairs across seeds; falsified if $\tau$ is flat or rising in $\phi$ while the shared component of LLM decisions ($\sigma_\eta^2$) is measurably positive \\
454+H2 & Order-flow correlation and synchrony rise in $\phi$, convexly (Prop.~\ref{prop:corr}) & $+$, convex & $\bar\rho$ and same-sign share across LLM pairs vs.\ \texttt{llm\_share}; falsified if the same-model pair correlation is not above the cross-model pair correlation \\
455+H3 & Bubble/crash frequency and liquidity spirals rise with $\phi\times$leverage (Props.~\ref{prop:feedback}, \ref{prop:spiral}) & interaction $+$ & frequency of band hits / forced-liquidation cascades, crash depth, in the \texttt{leveraged}$>0$ arms; falsified if the interaction is zero while $\hat\lambda X/m$ is observed to cross one more often under high $\phi$ \\
456+H4 & Prompt or model diversity restores part of the loss (Prop.~\ref{prop:diversity}) & $\tau_{\text{diverse}}>\tau_{\text{single}}$; partial & difference in $\tau$ between \texttt{prompt\_set=diverse} / mixed \texttt{provider\_mix} and single-model arms at equal \texttt{llm\_share}; falsified if diversity yields no gain, or a gain exceeding that of the fully private benchmark (which the model rules out) \\
457+H5 & Communication channels raise synchrony (Prop.~\ref{prop:corr} with $\sigma_\eta^2$ raised by shared rationales) & $+$ & $\bar S$ within cohorts with \texttt{llm.channel=true} vs.\ false; falsified if same-cohort synchrony does not exceed cross-cohort synchrony \\
458+H6 & Circuit breakers and position limits cut crash depth at a cost in time-to-absorb (Prop.~\ref{prop:safeguards}) & depth $-$; $T_\epsilon$ $+$ & crash depth and ticks-to-absorb under \texttt{halt\_threshold} and position caps vs.\ none; falsified if halts reduce depth with no delay (would reject A7 as the channel) or if position limits below $m/\hat\lambda$ fail to remove cascades \\
459+\hline
460+\end{tabular}
461+\end{table}
462+
463+Three further implications are useful as placebo or dose--response checks. (a) By
464+Proposition~\ref{prop:corr}, an LLM treatment whose decisions carry no common component
465+($\sigma_\eta^2=0$, e.g.\ the mock provider with independent seeds) should produce none of H1--H3; a
466+positive effect there would indicate a channel outside the model (for instance, a change in $\beta$ or in
467+latency). (b) By Proposition~\ref{prop:info}(ii), the loss of informativeness should not shrink as
468+$N$ grows from $100$ to $10{,}000$ at fixed $\phi$, whereas by (iii) it should vanish in the private-model
469+benchmark. (c) By Corollary~\ref{cor:llmtrend}, amplification should respond to $\phi$ only in treatments
470+where the measured return loading $\kappa$ of LLM decisions is positive.
added paper/sections/titlepage.tex +77 −0
@@ -0,0 +1,77 @@
1+% ============================================================================
2+% Title Page
3+% ============================================================================
4+\thispagestyle{empty}
5+
6+\begin{center}
7+
8+% --- Logo ---
9+\includegraphics[width=3.5cm]{uq_logo.jpg}
10+
11+\vspace{0.6cm}
12+
13+{\footnotesize\textsc{Université du Québec en Outaouais}}\\[0.15cm]
14+{\footnotesize\textsc{Département des sciences administratives}}
15+
16+\vspace{0.8cm}
17+
18+{\footnotesize\textsc{Working Paper No.~\WPnumber}}
19+
20+\vspace{1.2cm}
21+
22+% --- Title ---
23+{\LARGE\bfseries \WPtitle\par}
24+
25+\ifx\WPsubtitle\empty\else
26+ \vspace{0.3cm}
27+ {\large\itshape \WPsubtitle\par}
28+\fi
29+
30+\vspace{1.2cm}
31+
32+% --- Author ---
33+{\large \WPauthor}\\[0.3cm]
34+{\normalsize \WPaffiliation}\\[0.15cm]
35+{\normalsize \href{mailto:\WPemail}{\WPemail}}\\[0.15cm]
36+{\small \WPaddress}
37+
38+\vspace{0.8cm}
39+
40+% --- Date & Version ---
41+{\normalsize \WPdate}\\[0.1cm]
42+{\small Version~\WPversion}
43+
44+\end{center}
45+
46+\vfill
47+
48+\newpage
49+
50+% ============================================================================
51+% Abstract Page
52+% ============================================================================
53+\thispagestyle{empty}
54+
55+\vspace*{1cm}
56+
57+\noindent\rule{\textwidth}{0.4pt}
58+\vspace{0.3cm}
59+
60+\noindent\textbf{Abstract}
61+
62+\vspace{0.15cm}
63+
64+\noindent\WPabstract
65+
66+\vspace{0.4cm}
67+
68+\noindent\textbf{Keywords:} \WPkeywords
69+
70+\vspace{0.15cm}
71+
72+\noindent\textbf{JEL Classification:} \WPjel
73+
74+\vspace{0.3cm}
75+\noindent\rule{\textwidth}{0.4pt}
76+
77+\newpage
added paper/sections/titlepage_anon.tex +53 −0
@@ -0,0 +1,53 @@
1+% ============================================================================
2+% ANONYMOUS TITLE PAGE — for double-anonymized journal submission
3+% Used by paper/main_anon.tex (same macros as main.tex, no author block,
4+% no institution, no logo). Abstract/keywords/JEL page identical to titlepage.tex.
5+% ============================================================================
6+\thispagestyle{empty}
7+\begin{center}
8+
9+\vspace*{3cm}
10+
11+{\LARGE\bfseries \WPtitle\par}
12+
13+\ifx\WPsubtitle\empty\else
14+ \vspace{0.3cm}
15+ {\large\itshape \WPsubtitle\par}
16+\fi
17+
18+\vspace{1.5cm}
19+
20+{\normalsize [Author identity withheld for review]}
21+
22+\vspace{0.8cm}
23+
24+{\normalsize \WPdate}
25+
26+\end{center}
27+
28+\vfill
29+
30+\newpage
31+\thispagestyle{empty}
32+\vspace*{1cm}
33+
34+\noindent\rule{\textwidth}{0.4pt}
35+\vspace{0.3cm}
36+
37+\noindent\textbf{Abstract}
38+\vspace{0.15cm}
39+
40+\noindent\WPabstract
41+
42+\vspace{0.4cm}
43+
44+\noindent\textbf{Keywords:} \WPkeywords
45+
46+\vspace{0.15cm}
47+
48+\noindent\textbf{JEL Classification:} \WPjel
49+
50+\vspace{0.3cm}
51+\noindent\rule{\textwidth}{0.4pt}
52+
53+\newpage
added paper/tex2docx/README.md +26 −0
@@ -0,0 +1,26 @@
1+# tex2docx — conversion d'un working paper UQO (LaTeX) en Word
2+
3+Recette utilisée le 2026-10-26 pour le WP 17 (ai-disclosure-stock-returns). Le PDF doit
4+avoir été compilé d'abord (latexmk) : la numérotation des sections, tableaux, figures et
5+équations est reprise de `main.aux`, car pandoc seul perd les références croisées.
6+
7+```bash
8+cd <projet>/paper
9+latexmk # produit main.aux à jour
10+python3 -I <ici>/prep_tex.py . _pandoc_main.tex
11+pandoc _pandoc_main.tex -f latex -t docx --citeproc --bibliography references.bib \
12+ --resource-path=.:figures -M reference-section-title=References -M lang=en-US \
13+ -o ../<Nom>_WP<nn>_v<ver>_<date>.docx
14+python3 -I <ici>/post_docx.py ../<Nom>_WP<nn>_v<ver>_<date>.docx # python-docx
15+rm _pandoc_main.tex
16+```
17+
18+- `prep_tex.py` : aplatit les `\input`, retire les commentaires, résout `\ref`/`\eqref`,
19+ préfixe les légendes (« Table 5: », « Figure A1: »), numérote les titres et les équations,
20+ retire `\ifx` (sous-titre), `appendices`, `\cmidrule`, et ajoute des shims (`\num`,
21+ `\tablelegend`, `\tstat`, `\textperiodcentered`). Adapter le titre/les shims à chaque WP.
22+- `post_docx.py` : centre la page titre, déplace les références avant les annexes (pandoc
23+ les met en fin de document), pose les sauts de page, remplit les propriétés du document.
24+ Le titre et les métadonnées y sont codés en dur : à modifier pour un autre WP.
25+- Vérification visuelle : Word (sandboxé) refuse d'écrire dans /tmp ; exporter en PDF via
26+ AppleScript vers `~/Library/Containers/com.microsoft.Word/Data/` puis `pdftoppm`.
added paper/tex2docx/post_docx.py +41 −0
@@ -0,0 +1,41 @@
1+import sys, re, datetime
2+from docx import Document
3+from docx.shared import Pt
4+from docx.enum.text import WD_ALIGN_PARAGRAPH
5+path = sys.argv[1]; doc = Document(path)
6+paras = doc.paragraphs
7+norm = lambda p: re.sub(r"\s+", " ", p.text.replace(" ", " ")).strip()
8+title = "Does the Language of Artificial Intelligence Move Markets?"
9+# --- title page: everything before the Abstract block is centred
10+i_abs = next(i for i, p in enumerate(paras) if norm(p) == "Abstract")
11+for p in paras[:i_abs - 1]:
12+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
13+ if norm(p) == title:
14+ for r in p.runs: r.font.size = Pt(18); r.font.bold = True
15+ elif norm(p).startswith("A Semantic Analysis"):
16+ for r in p.runs: r.font.size = Pt(13)
17+# --- move References (pandoc appends it at the end) before the appendix, as in the PDF
18+h1 = [p for p in paras if p.style.name == "Heading 1"]
19+refs = next(p for p in h1 if norm(p) == "References")
20+appA = next(p for p in h1 if re.match(r"^A [A-Z]", norm(p)))
21+body = doc.element.body; kids = list(body)
22+i_refs, i_app = kids.index(refs._p), kids.index(appA._p)
23+moved = [k for k in kids[i_refs:] if k.tag.endswith("}sectPr") is False]
24+for k in moved: appA._p.addprevious(k)
25+# --- page breaks where the LaTeX source has \newpage
26+breaks = 0
27+paras[i_abs - 1].paragraph_format.page_break_before = True; breaks += 1
28+for p in h1:
29+ t = norm(p)
30+ if t.startswith("1 Introduction") or t == "References" or p is appA:
31+ p.paragraph_format.page_break_before = True; breaks += 1
32+# --- document properties
33+cp = doc.core_properties
34+cp.title = title
35+cp.subject = "A Semantic Analysis of S&P 500 SEC Filings and the Cross-Section of Stock Returns, 2010–2026"
36+cp.author = "Manel Kammoun; Djerrycharli Tandjambianda; Simon-Pierre Boucher"
37+cp.keywords = "textual analysis, sentence embeddings, artificial intelligence, disclosure, cross-section of returns"
38+cp.comments = "UQO Working Paper No. 17, Version 1.1, October 26, 2026"
39+cp.created = cp.modified = datetime.datetime(2026, 10, 26, 12, 0)
40+doc.save(path)
41+print(f"post-processed: {i_abs-1} title-page paragraphs centred, {len(moved)} reference paragraphs moved before appendix, {breaks} page breaks, properties set")
added paper/tex2docx/prep_tex.py +134 −0
@@ -0,0 +1,134 @@
1+"""Flatten a LaTeX working paper and resolve numbering from main.aux so that
2+pandoc produces a Word file with correct section/table/figure/equation numbers."""
3+import re, sys, pathlib
4+
5+PAPER = pathlib.Path(sys.argv[1]).resolve()
6+OUT = pathlib.Path(sys.argv[2])
7+warn = lambda *a: print("WARN:", *a, file=sys.stderr)
8+
9+def strip_comments(txt):
10+ out = []
11+ for line in txt.splitlines():
12+ if re.match(r'^\s*%', line):
13+ continue
14+ # drop trailing unescaped % comments
15+ line = re.sub(r'(?<!\\)%.*$', '', line)
16+ out.append(line)
17+ return "\n".join(out) + "\n"
18+
19+def inline(path):
20+ txt = strip_comments(path.read_text())
21+ def rep(m):
22+ name = m.group(1)
23+ p = PAPER / (name if name.endswith('.tex') else name + '.tex')
24+ if not p.exists():
25+ warn("missing input", p); return m.group(0)
26+ return inline(p)
27+ return re.sub(r'\\input\{([^}]+)\}', rep, txt)
28+
29+def brace_arg(s, i):
30+ """s[i] == '{' -> return (content, index after closing brace)"""
31+ assert s[i] == '{', s[i:i+20]
32+ depth = 0
33+ for j in range(i, len(s)):
34+ if s[j] == '{' and (j == 0 or s[j-1] != '\\'): depth += 1
35+ elif s[j] == '}' and s[j-1] != '\\':
36+ depth -= 1
37+ if depth == 0:
38+ return s[i+1:j], j+1
39+ raise ValueError("unbalanced braces")
40+
41+# ---------------------------------------------------------------- aux
42+aux = (PAPER / 'main.aux').read_text()
43+labels = {}
44+for m in re.finditer(r'\\newlabel\{([^}]+)\}\{', aux):
45+ outer, _ = brace_arg(aux, m.end()-1)
46+ num, _ = brace_arg(outer, 0)
47+ labels[m.group(1)] = num.strip()
48+toc = []
49+for m in re.finditer(r'\\contentsline \{(section|subsection|subsubsection)\}\{\\numberline \{([^}]*)\}(.*?)\}\{\d+\}\{', aux):
50+ toc.append((m.group(1), m.group(2), m.group(3)))
51+print(f"aux: {len(labels)} labels, {len(toc)} numbered headings")
52+
53+# ---------------------------------------------------------------- flatten
54+tex = inline(PAPER / 'main.tex')
55+
56+# titlepage: pandoc cannot evaluate \ifx -> keep the \else branch only
57+tex = re.sub(r'\\ifx\\WPsubtitle\\empty\\else(.*?)\\fi', r'\1', tex, flags=re.S)
58+
59+# appendix environment / counters that pandoc does not understand
60+tex = re.sub(r'\\(begin|end)\{appendices\}', '', tex)
61+tex = re.sub(r'\\renewcommand\{\\the(table|figure)\}\{[^{}]*(\{[^{}]*\})*[^{}]*\}', '', tex)
62+tex = re.sub(r'\\setcounter\{(table|figure)\}\{0\}', '', tex)
63+
64+# ---------------------------------------------------------------- headings
65+pos = 0; out = []; ti = 0
66+pat = re.compile(r'\\(section|subsection|subsubsection)\{')
67+while True:
68+ m = pat.search(tex, pos)
69+ if not m: break
70+ title, end = brace_arg(tex, m.end()-1)
71+ out.append(tex[pos:m.start()])
72+ lvl = m.group(1)
73+ if ti < len(toc) and toc[ti][0] == lvl:
74+ num = toc[ti][1]; ti += 1
75+ if re.sub(r'\W', '', toc[ti-1][2])[:12] != re.sub(r'\W', '', title)[:12]:
76+ warn("heading mismatch:", repr(toc[ti-1][2][:40]), "vs", repr(title[:40]))
77+ out.append(f"\\{lvl}{{{num}~{title}}}")
78+ else:
79+ warn("unnumbered heading kept:", lvl, title[:40])
80+ out.append(tex[m.start():end])
81+ pos = end
82+out.append(tex[pos:]); tex = "".join(out)
83+if ti != len(toc): warn(f"only {ti}/{len(toc)} toc entries consumed")
84+
85+# ---------------------------------------------------------------- floats: caption prefix
86+def fix_float(m):
87+ kind, body = m.group(1), m.group(2)
88+ labs = [l for l in re.findall(r'\\label\{([^}]+)\}', body) if l in labels]
89+ if not labs:
90+ warn("float without known label", body[:60]); return m.group(0)
91+ num = labels[labs[0]]
92+ word = 'Table' if kind == 'table' else 'Figure'
93+ cm = re.search(r'\\caption(\[[^\]]*\])?\{', body)
94+ if not cm: return m.group(0)
95+ cap, end = brace_arg(body, cm.end()-1)
96+ body = body[:cm.start()] + f"\\caption{{{word} {num}: {cap}}}" + body[end:]
97+ return f"\\begin{{{kind}}}{body}\\end{{{kind}}}"
98+tex = re.sub(r'\\begin\{(table|figure)\}(.*?)\\end\{\1\}', fix_float, tex, flags=re.S)
99+
100+# ---------------------------------------------------------------- equations: number tag
101+def fix_eq(m):
102+ body = m.group(1)
103+ lm = re.search(r'\\label\{([^}]+)\}', body)
104+ if lm and lm.group(1) in labels:
105+ body = body.replace(lm.group(0), '')
106+ body = body.rstrip() + f"\\qquad\\text{{({labels[lm.group(1)]})}}\n"
107+ return "\\begin{equation}" + body + "\\end{equation}"
108+tex = re.sub(r'\\begin\{equation\}(.*?)\\end\{equation\}', fix_eq, tex, flags=re.S)
109+
110+# ---------------------------------------------------------------- cross-references
111+missing = set()
112+def ref(m):
113+ k = m.group(2)
114+ if k not in labels:
115+ missing.add(k); return '??'
116+ return f"({labels[k]})" if m.group(1) == 'eqref' else labels[k]
117+tex = re.sub(r'\\(eqref|ref)\{([^}]+)\}', ref, tex)
118+if missing: warn("unresolved labels:", sorted(missing))
119+tex = re.sub(r'\\label\{[^}]+\}', '', tex)
120+
121+# booktabs partial rules have no Word equivalent and leak as text
122+tex = re.sub(r'\\cmidrule(\([^)]*\))?\{[^}]*\}', '', tex)
123+
124+# ---------------------------------------------------------------- pandoc shims
125+shims = r"""
126+\newcommand{\num}[1]{#1}
127+\renewcommand{\tablelegend}[1]{\par\textit{Notes:} #1}
128+\renewcommand{\tstat}[1]{(\textit{t}=#1)}
129+\newcommand{\textperiodcentered}{·}
130+"""
131+tex = tex.replace("\\begin{document}", shims + "\\begin{document}", 1)
132+tex = re.sub(r'(\\newcommand\{\\WPauthor\}\{[^}]*)\}', lambda m: m.group(1).replace('\\quad', '~~~~') + '}', tex)
133+OUT.write_text(tex)
134+print("wrote", OUT, len(tex), "chars")
added paper/uq_logo.jpg +0 −0

Binary file not shown.

added pyproject.toml +45 −0
@@ -0,0 +1,45 @@
1+[project]
2+name = "market-agents"
3+version = "0.1.0"
4+description = "The Invisible Hand of Machines: price discovery in markets populated by LLM agents (UQO WP 27) — event-sourced LOB simulator, classical and LLM agent populations, randomized experiments."
5+readme = "README.md"
6+requires-python = ">=3.12"
7+license = { text = "MIT" }
8+authors = [{ name = "Simon-Pierre Boucher", email = "simon-pierre.boucher@uqo.ca" }]
9+dependencies = [
10+ "numpy>=2.0",
11+ "pandas>=2.2",
12+ "scipy>=1.14",
13+ "statsmodels>=0.14",
14+ "pydantic>=2.8",
15+ "pyyaml>=6.0",
16+ "duckdb>=1.1",
17+ "pyarrow>=17",
18+ "matplotlib>=3.9",
19+ "sortedcontainers>=2.4",
20+ "openai>=1.50",
21+ "anthropic>=0.40",
22+ "python-dotenv>=1.0",
23+ "tqdm>=4.66",
24+]
25+
26+[project.optional-dependencies]
27+dev = ["pytest>=8", "hypothesis>=6.100", "pytest-xdist>=3.6", "ruff>=0.6"]
28+
29+[project.scripts]
30+ma = "market_agents.cli:main"
31+
32+[build-system]
33+requires = ["hatchling"]
34+build-backend = "hatchling.build"
35+
36+[tool.hatch.build.targets.wheel]
37+packages = ["src/market_agents"]
38+
39+[tool.pytest.ini_options]
40+testpaths = ["tests"]
41+addopts = "-q"
42+
43+[tool.ruff]
44+line-length = 110
45+target-version = "py312"
added results/calibration/scale_benchmark.json +373 −0
@@ -0,0 +1,373 @@
1+[
2+ {
3+ "name": "scale_100",
4+ "seed": 1,
5+ "config_hash": "72ec6dde7c77c2e9",
6+ "n_traders": 100,
7+ "llm_share": 0.0,
8+ "llm_share_traders": 0.0,
9+ "llm_share_orders": 0.0,
10+ "llm_share_volume": 0.0,
11+ "n_llm_decisions": 0,
12+ "n_trades": 10295,
13+ "wall_s": 0.5701620420441031,
14+ "msd_log": 0.00019286955904584808,
15+ "mad_pct": 0.7310450811973923,
16+ "bias_pct": -0.15575191205753852,
17+ "vr5": 1.0718532448575135,
18+ "vr10": 1.2475793867974083,
19+ "vol_bp": 32.35812080767805,
20+ "kurtosis": 10.234149771580848,
21+ "acf_absr_1_10": 2.725430443480742,
22+ "acf_r_1": 0.040723476211249214,
23+ "spread_bp": 28.878447094188143,
24+ "eff_spread_bp": 41.4787058741681,
25+ "depth5": 132.712,
26+ "lambda_bp_100": 20.621638192405012,
27+ "resiliency": 0.6471442312753932,
28+ "n_bubbles": 1,
29+ "bubble_amp": 0.080067123441383,
30+ "bubble_dur": 13.0,
31+ "max_dd": 0.05210182210339487,
32+ "n_crashes": 0,
33+ "volume": 57653.0,
34+ "n_halts": 0,
35+ "share_ticks_halted": 0.0,
36+ "time_to_absorb": 24.0,
37+ "response_ratio": 1.0602789303452547,
38+ "pnl_fundamentalist": 17.656477071443046,
39+ "pnl_leveraged": 8.519706421361073,
40+ "pnl_market_maker": 9.050950394737617,
41+ "pnl_meanrev": 4.209786017783919,
42+ "pnl_noise": 7.296227416578166,
43+ "pnl_trend": 8.888541562040968,
44+ "gini_final": 0.27427592144984314,
45+ "gini_initial": 0.2804114796674862,
46+ "n_bankrupt": 0,
47+ "cancel_ratio": 0.15356339084771192,
48+ "reject_ratio": 0.010352588147036759,
49+ "fill_ratio": 1.54463615903976,
50+ "turnover": 5.643414799728445,
51+ "mm_pnl": 9.050950394737619,
52+ "mm_inventory_abs": 447.8,
53+ "n_orders": 13330
54+ },
55+ {
56+ "name": "scale_100",
57+ "seed": 2,
58+ "config_hash": "f50aac64012c79cc",
59+ "n_traders": 100,
60+ "llm_share": 0.0,
61+ "llm_share_traders": 0.0,
62+ "llm_share_orders": 0.0,
63+ "llm_share_volume": 0.0,
64+ "n_llm_decisions": 0,
65+ "n_trades": 10527,
66+ "wall_s": 0.5802934590028599,
67+ "msd_log": 0.0002888292585473304,
68+ "mad_pct": 0.9452873073912066,
69+ "bias_pct": -0.29914538754731906,
70+ "vr5": 1.5658770854218884,
71+ "vr10": 2.2323861312360114,
72+ "vol_bp": 19.93022805615968,
73+ "kurtosis": 14.340120252000448,
74+ "acf_absr_1_10": 2.5967756236642745,
75+ "acf_r_1": 0.20815586356877122,
76+ "spread_bp": 24.364181008850423,
77+ "eff_spread_bp": 34.16487033612486,
78+ "depth5": 146.782,
79+ "lambda_bp_100": 12.733686146838773,
80+ "resiliency": 0.7132913644500204,
81+ "n_bubbles": 1,
82+ "bubble_amp": 0.07461228907838036,
83+ "bubble_dur": 22.0,
84+ "max_dd": 0.04207449700924415,
85+ "n_crashes": 0,
86+ "volume": 61390.0,
87+ "n_halts": 0,
88+ "share_ticks_halted": 0.0,
89+ "time_to_absorb": 39.0,
90+ "response_ratio": 0.31568646155126934,
91+ "pnl_fundamentalist": 12.858237774095874,
92+ "pnl_leveraged": 8.307969157398325,
93+ "pnl_market_maker": 7.757582637283514,
94+ "pnl_meanrev": 4.448602248693099,
95+ "pnl_noise": 6.090876220549884,
96+ "pnl_trend": 6.275541672164394,
97+ "gini_final": 0.3192748996527118,
98+ "gini_initial": 0.3167311413076369,
99+ "n_bankrupt": 0,
100+ "cancel_ratio": 0.17202363793600461,
101+ "reject_ratio": 0.011746901124243299,
102+ "fill_ratio": 1.5172960507350821,
103+ "turnover": 5.789452166802944,
104+ "mm_pnl": 7.757582637283514,
105+ "mm_inventory_abs": 537.6,
106+ "n_orders": 13876
107+ },
108+ {
109+ "name": "scale_100",
110+ "seed": 3,
111+ "config_hash": "3d20d4f33dca1657",
112+ "n_traders": 100,
113+ "llm_share": 0.0,
114+ "llm_share_traders": 0.0,
115+ "llm_share_orders": 0.0,
116+ "llm_share_volume": 0.0,
117+ "n_llm_decisions": 0,
118+ "n_trades": 10741,
119+ "wall_s": 0.581291499955114,
120+ "msd_log": 0.0001581647166691645,
121+ "mad_pct": 0.7940933085313311,
122+ "bias_pct": -0.1457811073728486,
123+ "vr5": 1.4253673844532706,
124+ "vr10": 2.1997897604423264,
125+ "vol_bp": 18.747558012139187,
126+ "kurtosis": 6.081260113112632,
127+ "acf_absr_1_10": 2.4869862608002076,
128+ "acf_r_1": 0.08038707277238923,
129+ "spread_bp": 23.159298170155786,
130+ "eff_spread_bp": 32.62440165728167,
131+ "depth5": 145.45,
132+ "lambda_bp_100": 9.286127919988914,
133+ "resiliency": 0.7155986676930891,
134+ "n_bubbles": 0,
135+ "bubble_amp": 0.0,
136+ "bubble_dur": 0.0,
137+ "max_dd": 0.030664395229982988,
138+ "n_crashes": 0,
139+ "volume": 62973.0,
140+ "n_halts": 0,
141+ "share_ticks_halted": 0.0,
142+ "time_to_absorb": 26.0,
143+ "response_ratio": 0.5264625844839756,
144+ "pnl_fundamentalist": 13.05203477140921,
145+ "pnl_leveraged": 7.137665260941994,
146+ "pnl_market_maker": 9.437974794622459,
147+ "pnl_meanrev": 8.854861916786305,
148+ "pnl_noise": 8.383088043861747,
149+ "pnl_trend": 6.794934289244118,
150+ "gini_final": 0.28354147912478633,
151+ "gini_initial": 0.2820805035864161,
152+ "n_bankrupt": 0,
153+ "cancel_ratio": 0.15596595007935363,
154+ "reject_ratio": 0.009955273409320444,
155+ "fill_ratio": 1.5497042273842159,
156+ "turnover": 5.99405210689453,
157+ "mm_pnl": 9.437974794622459,
158+ "mm_inventory_abs": 508.8,
159+ "n_orders": 13862
160+ },
161+ {
162+ "name": "scale_1000",
163+ "seed": 1,
164+ "config_hash": "ee14f7c7fdd0c799",
165+ "n_traders": 1000,
166+ "llm_share": 0.0,
167+ "llm_share_traders": 0.0,
168+ "llm_share_orders": 0.0,
169+ "llm_share_volume": 0.0,
170+ "n_llm_decisions": 0,
171+ "n_trades": 11682,
172+ "wall_s": 2.670115082990378,
173+ "msd_log": 0.00018062420246409605,
174+ "mad_pct": 0.8500964158284774,
175+ "bias_pct": -0.18426548373374133,
176+ "vr5": 2.025387100878808,
177+ "vr10": 3.2959357937833555,
178+ "vol_bp": 13.822478939583142,
179+ "kurtosis": 8.487363422007448,
180+ "acf_absr_1_10": 3.676658722746514,
181+ "acf_r_1": 0.287550868207432,
182+ "spread_bp": 13.824129777426267,
183+ "eff_spread_bp": 20.735460509724028,
184+ "depth5": 221.702,
185+ "lambda_bp_100": 4.820764798328324,
186+ "resiliency": 0.7153936557542477,
187+ "n_bubbles": 1,
188+ "bubble_amp": 0.07026257865057628,
189+ "bubble_dur": 11.0,
190+ "max_dd": 0.06061815440556961,
191+ "n_crashes": 0,
192+ "volume": 71448.0,
193+ "n_halts": 0,
194+ "share_ticks_halted": 0.0,
195+ "time_to_absorb": 24.0,
196+ "response_ratio": 0.6796255487489661,
197+ "pnl_fundamentalist": 12.22118924232089,
198+ "pnl_leveraged": 8.85051472713129,
199+ "pnl_market_maker": 8.39873997810738,
200+ "pnl_meanrev": 4.758693065304863,
201+ "pnl_noise": 7.802552958684208,
202+ "pnl_trend": 8.986276033348616,
203+ "gini_final": 0.2858965594076205,
204+ "gini_initial": 0.2855288797803193,
205+ "n_bankrupt": 0,
206+ "cancel_ratio": 0.0687326986699075,
207+ "reject_ratio": 0.006886773344136115,
208+ "fill_ratio": 1.5774762001215312,
209+ "turnover": 0.6852169116725306,
210+ "mm_pnl": 8.398739978107383,
211+ "mm_inventory_abs": 493.8,
212+ "n_orders": 14811
213+ },
214+ {
215+ "name": "scale_1000",
216+ "seed": 2,
217+ "config_hash": "b16091b8bb39498f",
218+ "n_traders": 1000,
219+ "llm_share": 0.0,
220+ "llm_share_traders": 0.0,
221+ "llm_share_orders": 0.0,
222+ "llm_share_volume": 0.0,
223+ "n_llm_decisions": 0,
224+ "n_trades": 11629,
225+ "wall_s": 2.6595235830172896,
226+ "msd_log": 0.00041330390214059054,
227+ "mad_pct": 1.1917097910197463,
228+ "bias_pct": -0.15970070791396937,
229+ "vr5": 1.644328760768095,
230+ "vr10": 2.756869312033638,
231+ "vol_bp": 12.118437319273784,
232+ "kurtosis": 14.604102013472062,
233+ "acf_absr_1_10": 2.6836877474394876,
234+ "acf_r_1": 0.02149103385973521,
235+ "spread_bp": 12.264625299683127,
236+ "eff_spread_bp": 19.045456466147648,
237+ "depth5": 243.088,
238+ "lambda_bp_100": 5.2096578366086845,
239+ "resiliency": 0.5268311406120124,
240+ "n_bubbles": 1,
241+ "bubble_amp": 0.07560512002649855,
242+ "bubble_dur": 33.0,
243+ "max_dd": 0.026409804187581987,
244+ "n_crashes": 0,
245+ "volume": 72963.0,
246+ "n_halts": 0,
247+ "share_ticks_halted": 0.0,
248+ "time_to_absorb": 55.0,
249+ "response_ratio": 0.23023340213135676,
250+ "pnl_fundamentalist": 9.410087991005573,
251+ "pnl_leveraged": 8.06978747113479,
252+ "pnl_market_maker": 8.58296964348504,
253+ "pnl_meanrev": 7.3927831227808385,
254+ "pnl_noise": 8.868638893094134,
255+ "pnl_trend": 9.12795706441179,
256+ "gini_final": 0.2822291008133873,
257+ "gini_initial": 0.2821557785325072,
258+ "n_bankrupt": 0,
259+ "cancel_ratio": 0.07004048582995952,
260+ "reject_ratio": 0.011201079622132253,
261+ "fill_ratio": 1.569365721997301,
262+ "turnover": 0.6991975582965093,
263+ "mm_pnl": 8.58296964348504,
264+ "mm_inventory_abs": 483.22,
265+ "n_orders": 14820
266+ },
267+ {
268+ "name": "scale_1000",
269+ "seed": 3,
270+ "config_hash": "bb000b72ba320264",
271+ "n_traders": 1000,
272+ "llm_share": 0.0,
273+ "llm_share_traders": 0.0,
274+ "llm_share_orders": 0.0,
275+ "llm_share_volume": 0.0,
276+ "n_llm_decisions": 0,
277+ "n_trades": 12209,
278+ "wall_s": 2.7417157079908065,
279+ "msd_log": 0.00021092782536451138,
280+ "mad_pct": 0.9236328359261194,
281+ "bias_pct": -0.17586691384915412,
282+ "vr5": 1.611910893920555,
283+ "vr10": 2.6070797852167216,
284+ "vol_bp": 13.820990551125412,
285+ "kurtosis": 4.406283908221623,
286+ "acf_absr_1_10": 1.749659430254234,
287+ "acf_r_1": 0.14228872111193444,
288+ "spread_bp": 16.402115035292773,
289+ "eff_spread_bp": 24.921946560889914,
290+ "depth5": 178.83,
291+ "lambda_bp_100": 6.947876984339655,
292+ "resiliency": 0.5644120216956088,
293+ "n_bubbles": 1,
294+ "bubble_amp": 0.06792151254244383,
295+ "bubble_dur": 11.0,
296+ "max_dd": 0.03714749884823354,
297+ "n_crashes": 0,
298+ "volume": 77495.0,
299+ "n_halts": 0,
300+ "share_ticks_halted": 0.0,
301+ "time_to_absorb": 38.0,
302+ "response_ratio": 0.38870487487733535,
303+ "pnl_fundamentalist": 11.763531925432737,
304+ "pnl_leveraged": 12.30986340715177,
305+ "pnl_market_maker": 9.441394250152205,
306+ "pnl_meanrev": 7.062613962399257,
307+ "pnl_noise": 8.798699442629827,
308+ "pnl_trend": 11.794205881549368,
309+ "gini_final": 0.2887824703843839,
310+ "gini_initial": 0.28816811800295694,
311+ "n_bankrupt": 0,
312+ "cancel_ratio": 0.06202264946010008,
313+ "reject_ratio": 0.011324730050039505,
314+ "fill_ratio": 1.607716618382934,
315+ "turnover": 0.7320112276259568,
316+ "mm_pnl": 9.441394250152207,
317+ "mm_inventory_abs": 505.64,
318+ "n_orders": 15188
319+ },
320+ {
321+ "name": "scale_10000",
322+ "seed": 1,
323+ "config_hash": "b20b7ea1893420e2",
324+ "n_traders": 10000,
325+ "llm_share": 0.0,
326+ "llm_share_traders": 0.0,
327+ "llm_share_orders": 0.0,
328+ "llm_share_volume": 0.0,
329+ "n_llm_decisions": 0,
330+ "n_trades": 11885,
331+ "wall_s": 23.677518000011332,
332+ "msd_log": 0.0003067595731653351,
333+ "mad_pct": 1.2324887935614917,
334+ "bias_pct": -0.005074964908155267,
335+ "vr5": 1.5681375271456242,
336+ "vr10": 2.5294578246021526,
337+ "vol_bp": 10.572758594723124,
338+ "kurtosis": 4.752405591681221,
339+ "acf_absr_1_10": 3.335881681903191,
340+ "acf_r_1": 0.09791372200435924,
341+ "spread_bp": 10.897921046050657,
342+ "eff_spread_bp": 18.2037877380357,
343+ "depth5": 455.138,
344+ "lambda_bp_100": 2.6356634482239287,
345+ "resiliency": 0.379668239121855,
346+ "n_bubbles": 1,
347+ "bubble_amp": 0.06949448067157304,
348+ "bubble_dur": 16.0,
349+ "max_dd": 0.04199416623989671,
350+ "n_crashes": 0,
351+ "volume": 84806.0,
352+ "n_halts": 0,
353+ "share_ticks_halted": 0.0,
354+ "time_to_absorb": 48.0,
355+ "response_ratio": 0.3440702894130496,
356+ "pnl_fundamentalist": 9.880501047388295,
357+ "pnl_leveraged": 9.530656101327683,
358+ "pnl_market_maker": 9.1641097345847,
359+ "pnl_meanrev": 8.715541449604128,
360+ "pnl_noise": 9.212122901393542,
361+ "pnl_trend": 9.50732656220052,
362+ "gini_final": 0.28254323570340967,
363+ "gini_initial": 0.282654480272589,
364+ "n_bankrupt": 0,
365+ "cancel_ratio": 0.01489140330342047,
366+ "reject_ratio": 0.020548835999479775,
367+ "fill_ratio": 1.5457146573026401,
368+ "turnover": 0.08007531890809352,
369+ "mm_pnl": 9.1641097345847,
370+ "mm_inventory_abs": 480.866,
371+ "n_orders": 15378
372+ }
373+]
\ No newline at end of file
added results/calibration/sweep_summary.csv +37 −0
@@ -0,0 +1,37 @@
1+tr_fund_aggr,tr_trend_share,tr_noise_pm,tr_mm_qty,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
2+10.0,0.1,0.3,10,2.698,0.224,1.839,23.136,2.616,7.978,6.301,162.342,6.917,1.2,0.011,78.1,0.1,25228.5,7.271
3+10.0,0.1,0.3,20,2.406,0.324,2.167,15.965,3.04,6.607,6.286,167.461,5.796,1.0,0.009,70.2,0.145,33156.6,7.445
4+10.0,0.1,0.6,10,1.214,-0.047,1.166,28.66,1.986,29.876,27.839,108.39,16.617,1.0,0.054,47.5,0.427,51605.7,8.852
5+10.0,0.1,0.6,20,1.305,0.094,1.425,26.551,2.619,33.44,25.39,133.596,17.741,0.9,0.069,52.6,0.82,58637.0,8.279
6+10.0,0.15,0.3,10,2.345,0.317,2.158,21.267,3.163,7.31,5.688,154.374,7.377,1.0,0.012,66.3,0.144,24564.1,8.051
7+10.0,0.15,0.3,20,2.111,0.441,2.709,23.458,3.842,12.162,7.592,162.485,9.275,1.0,0.034,58.0,0.346,35963.7,7.762
8+10.0,0.15,0.6,10,1.574,-0.104,0.943,24.229,1.663,42.329,33.998,104.189,13.755,0.5,0.071,23.0,0.805,56672.3,8.47
9+10.0,0.15,0.6,20,1.89,-0.068,1.034,18.693,2.112,56.106,40.324,131.659,23.987,1.0,0.087,52.8,1.057,68174.1,7.565
10+10.0,0.25,0.3,10,2.75,0.176,1.751,22.688,3.196,16.265,8.577,147.645,10.563,1.2,0.048,60.3,0.229,29710.8,8.098
11+10.0,0.25,0.3,20,3.942,0.015,1.333,50.945,2.844,33.21,13.863,155.588,17.472,1.9,0.078,64.4,0.586,37391.3,6.227
12+10.0,0.25,0.6,10,2.974,-0.119,0.89,51.116,1.875,47.254,30.151,122.282,13.292,1.6,0.089,55.3,0.604,46969.2,7.893
13+10.0,0.25,0.6,20,3.869,-0.21,0.755,26.705,2.806,78.396,41.352,140.836,25.529,2.6,0.138,65.2,0.759,59707.4,8.35
14+20.0,0.1,0.3,10,2.845,0.183,1.738,13.742,2.354,4.229,5.654,162.888,4.125,1.0,0.005,86.8,0.122,25251.9,7.076
15+20.0,0.1,0.3,20,2.082,0.252,1.996,37.613,2.793,9.246,6.508,174.172,6.416,1.1,0.014,61.2,0.194,33446.3,7.642
16+20.0,0.1,0.6,10,0.993,-0.029,1.162,23.993,2.416,23.951,27.137,115.287,13.044,0.9,0.041,38.8,0.42,51833.1,8.632
17+20.0,0.1,0.6,20,0.992,0.015,1.295,26.641,2.709,30.071,24.105,140.887,15.617,0.8,0.052,35.9,0.708,59961.2,8.185
18+20.0,0.15,0.3,10,2.236,0.262,2.172,24.276,3.559,8.018,6.263,162.027,7.353,1.1,0.014,68.7,0.144,25386.7,7.706
19+20.0,0.15,0.3,20,2.177,0.216,1.941,29.122,3.207,14.069,9.231,166.845,9.358,1.1,0.033,64.3,0.362,37523.8,7.197
20+20.0,0.15,0.6,10,1.612,-0.168,0.83,23.084,1.749,47.691,37.721,113.049,20.276,0.9,0.073,31.2,0.729,60030.9,8.26
21+20.0,0.15,0.6,20,1.816,-0.046,1.062,21.746,2.335,46.259,34.551,141.014,21.747,1.2,0.073,55.6,0.823,64856.1,8.284
22+20.0,0.25,0.3,10,2.876,0.14,1.615,35.269,3.131,13.373,7.595,153.125,11.246,1.3,0.033,57.8,0.251,25627.0,7.861
23+20.0,0.25,0.3,20,3.342,0.075,1.447,26.958,3.146,23.134,11.902,158.462,11.366,1.4,0.057,74.4,0.335,38279.4,6.737
24+20.0,0.25,0.6,10,3.061,-0.22,0.673,32.356,2.351,47.425,31.269,120.021,12.673,1.4,0.082,58.6,0.569,48578.1,7.489
25+20.0,0.25,0.6,20,2.85,-0.158,0.827,32.742,2.543,66.437,37.852,143.207,16.436,2.4,0.106,53.6,0.801,60073.2,9.156
26+40.0,0.1,0.3,10,2.607,0.225,1.889,11.382,2.826,5.019,6.034,172.531,4.548,1.1,0.009,80.7,0.104,26005.6,7.195
27+40.0,0.1,0.3,20,2.572,0.299,1.986,12.065,2.432,5.7,6.598,178.882,5.285,1.0,0.011,77.6,0.123,33449.3,7.15
28+40.0,0.1,0.6,10,0.951,-0.19,0.924,33.591,1.976,29.117,31.713,128.648,17.456,1.0,0.039,42.7,0.361,52872.6,8.731
29+40.0,0.1,0.6,20,0.777,-0.018,1.269,36.12,2.95,26.271,24.487,148.269,12.677,0.7,0.044,34.9,0.773,60686.5,8.494
30+40.0,0.15,0.3,10,2.444,0.185,1.685,26.051,2.399,7.927,6.642,167.3,6.437,1.0,0.013,67.0,0.171,26182.2,7.321
31+40.0,0.15,0.3,20,2.774,0.278,1.929,14.153,2.495,7.336,6.7,173.731,5.658,0.9,0.014,71.8,0.291,32708.6,6.872
32+40.0,0.15,0.6,10,1.118,-0.214,0.823,28.983,2.198,43.606,36.385,123.25,14.558,1.1,0.056,38.2,0.532,60037.2,9.01
33+40.0,0.15,0.6,20,1.158,-0.235,0.752,27.588,2.143,52.854,35.691,149.249,19.608,0.6,0.076,45.2,0.9,72789.1,8.274
34+40.0,0.25,0.3,10,2.619,0.017,1.354,40.885,3.073,14.311,9.366,157.749,10.125,1.0,0.035,51.3,0.232,29271.7,7.698
35+40.0,0.25,0.3,20,3.854,0.05,1.319,33.607,3.085,16.394,9.267,165.294,9.221,1.3,0.025,80.9,0.263,31602.9,7.09
36+40.0,0.25,0.6,10,2.414,-0.297,0.598,31.775,2.025,55.508,38.555,126.512,15.333,0.9,0.08,42.5,0.574,56636.1,8.981
37+40.0,0.25,0.6,20,3.22,-0.212,0.694,30.719,2.315,60.433,39.998,145.301,16.475,1.7,0.091,71.6,0.569,65920.7,7.354
added results/figures/fig_baseline_paths.pdf +0 −0

Binary file not shown.

added results/figures/fig_baseline_paths.png +0 −0

Binary file not shown.

added results/figures/fig_classical_lambda_bp_100.pdf +0 −0

Binary file not shown.

added results/figures/fig_classical_lambda_bp_100.png +0 −0

Binary file not shown.

added results/figures/fig_classical_mad_pct.pdf +0 −0

Binary file not shown.

added results/figures/fig_classical_mad_pct.png +0 −0

Binary file not shown.

added results/figures/fig_classical_max_dd.pdf +0 −0

Binary file not shown.

added results/figures/fig_classical_max_dd.png +0 −0

Binary file not shown.

added results/figures/fig_classical_n_bubbles.pdf +0 −0

Binary file not shown.

added results/figures/fig_classical_n_bubbles.png +0 −0

Binary file not shown.

added results/figures/fig_classical_spread_bp.pdf +0 −0

Binary file not shown.

added results/figures/fig_classical_spread_bp.png +0 −0

Binary file not shown.

added results/figures/fig_classical_time_to_absorb.pdf +0 −0

Binary file not shown.

added results/figures/fig_classical_time_to_absorb.png +0 −0

Binary file not shown.

added results/figures/fig_contagion_excess.pdf +0 −0

Binary file not shown.

added results/figures/fig_contagion_excess.png +0 −0

Binary file not shown.

added results/figures/fig_contagion_spillover.pdf +0 −0

Binary file not shown.

added results/figures/fig_contagion_spillover.png +0 −0

Binary file not shown.

added results/figures/fig_wealth_depth.pdf +0 −0

Binary file not shown.

added results/figures/fig_wealth_depth.png +0 −0

Binary file not shown.

added results/runs/.gitkeep +0 −0
added results/tables/theory_checks.csv +89 −0
@@ -0,0 +1,89 @@
1+proposition,case,quantity,theory,simulation,rel_error
2+P1-P2,"single model, phi=0",tau,0.9589566551591868,0.9588458450583444,0.00011555277315851961
3+P1-P2,"single model, phi=0",lam,0.019179133103183737,0.019175752481317396,0.0001762656241109893
4+P1-P2,"single model, phi=0",mse,0.04104334484081318,0.04129126465884099,0.006040438930827145
5+P1-P2,"single model, phi=0",rhobar,0.3787878787878788,0.37947533265704547,0.0018148782146000552
6+P1-P2,"single model, phi=0",sync,0.6236590070768526,0.6237815836734695,0.0001965442577208661
7+P1-P2,"single model, phi=0.1",tau,0.9516558812333461,0.9516118476031707,4.6270538588263265e-05
8+P1-P2,"single model, phi=0.1",lam,0.01903311762466692,0.019018044989802807,0.0007919162357605526
9+P1-P2,"single model, phi=0.1",mse,0.0483441187666539,0.04828641424547546,0.001193620292407563
10+P1-P2,"single model, phi=0.1",rhobar,0.3818800247371676,0.3817584348587759,0.00031839811070336966
11+P1-P2,"single model, phi=0.1",sync,0.6248831487706711,0.6249644571428571,0.00013011772256295257
12+P1-P2,"single model, phi=0.2",tau,0.9269558769002596,0.926803288755423,0.00016461209064977988
13+P1-P2,"single model, phi=0.2",lam,0.018539117538005193,0.01853558630569371,0.000190474670881426
14+P1-P2,"single model, phi=0.2",mse,0.0730441230997404,0.07321495387982196,0.0023387340806090497
15+P1-P2,"single model, phi=0.2",rhobar,0.39270253555967843,0.3927190039434333,4.1936026034011325e-05
16+P1-P2,"single model, phi=0.2",sync,0.6291676446990355,0.6290899428571428,0.0001234994242748715
17+P1-P2,"single model, phi=0.5",tau,0.7795447458684129,0.7814543864299311,0.0024496869123155607
18+P1-P2,"single model, phi=0.5",lam,0.015590894917368259,0.015603423881791543,0.000803607778109422
19+P1-P2,"single model, phi=0.5",mse,0.22045525413158706,0.21987004006908134,0.00265457071917377
20+P1-P2,"single model, phi=0.5",rhobar,0.471552257266543,0.47343561787020266,0.003993959470318295
21+P1-P2,"single model, phi=0.5",sync,0.6603832578914052,0.6608069224489797,0.0006415434560337463
22+P1-P2,"single model, phi=1",tau,0.49436424757761516,0.49184955889597654,0.005086712265218604
23+P1-P2,"single model, phi=1",lam,0.009887284951552304,0.009859406617159425,0.0028196147404957062
24+P1-P2,"single model, phi=1",mse,0.5056357524223849,0.5068518891714339,0.002405163683981475
25+P1-P2,"single model, phi=1",rhobar,0.7575757575757576,0.7571975042536527,0.000499294385178386
26+P1-P2,"single model, phi=1",sync,0.773616364569609,0.7735689387755101,6.130402130941514e-05
27+P1(i),tau strictly decreasing over phi grid (1=yes),monotone,1.0,1.0,0.0
28+P1(i),Omega(phi=1/N) / Omega(phi=0),ratio,1.0,1.0,0.0
29+P1(ii),"N->inf limit, phi=0.2",tau_inf,0.9615384615384615,0.9615383284021541,1.3846175960452457e-07
30+P1(ii),"bound tau(N=50)<=tau_inf, phi=0.2 (1=yes)",bound,1.0,1.0,0.0
31+P1(ii),"N->inf limit, phi=0.5",tau_inf,0.8,0.7999999270398466,9.120019184294392e-08
32+P1(ii),"bound tau(N=50)<=tau_inf, phi=0.5 (1=yes)",bound,1.0,1.0,0.0
33+P1(iii),"phi=0, N=1e7",tau->1,1.0,0.9999998359997768,1.640002231795279e-07
34+P2(ii),phi=0.5,rho_same,0.7575757575757576,0.7570963864431715,0.0006327698950136717
35+P2(ii),phi=0.5,rho_cross,0.3787878787878788,0.3779700431514435,0.0021590860801891454
36+P2(iv),"phi=0.5, same-model pair",S(rho1)=1/2+asin(rho1)/pi,0.773616364569609,0.7733414666666667,0.00035534137530203864
37+P2 placebo,sigma_eta=0: Omega(phi=1)/Omega(phi=0),ratio,1.0,1.0,0.0
38+P3,"Phi=1, G=1 (50)",tau,0.49436424757761516,0.49341141461729443,0.0019273905121367655
39+P3,"Phi=1, G=1 (50)",rhobar,0.7575757575757576,0.7569853104346328,0.0007793902262846997
40+P3,"Phi=1, G=1 (50)",sync,0.773616364569609,0.7736320367346939,2.025831639902203e-05
41+P3,"Phi=1, G=2 uniform (25,25)",tau,0.6566850538481743,0.6574905050550102,0.0012265410977696156
42+P3,"Phi=1, G=2 uniform (25,25)",rhobar,0.5643166357452072,0.5660294353951645,0.0030351748317599446
43+P3,"Phi=1, G=2 uniform (25,25)",sync,0.6971075087059577,0.6976120734693878,0.0007237976311095928
44+P3,"Phi=1, G=5 uniform (10x5)",tau,0.8177952240758913,0.8172375337609343,0.0006819437171294251
45+P3,"Phi=1, G=5 uniform (10x5)",rhobar,0.4483611626468769,0.44833526158669923,5.776829559631221e-05
46+P3,"Phi=1, G=5 uniform (10x5)",sync,0.6512021951877671,0.6512468040816326,6.85023702240756e-05
47+P3,"Phi=1, G=3 skewed (30,10,10)",tau,0.6836204539239814,0.6831971941831166,0.000619144348936982
48+P3,"Phi=1, G=3 skewed (30,10,10)",rhobar,0.5411255411255411,0.5400367440856554,0.0020120969297088404
49+P3,"Phi=1, G=3 skewed (30,10,10)",sync,0.6879264460023196,0.6872304367346939,0.0010117495433855585
50+P3,"Phi=0.6, G=1 (30)",tau,0.719010641357492,0.7188837757769287,0.00017644464944743162
51+P3,"Phi=0.6, G=1 (30)",rhobar,0.5132962275819418,0.513237234068623,0.00011493073618862985
52+P3,"Phi=0.6, G=1 (30)",sync,0.6769091707579539,0.6769316775510205,3.324935462373573e-05
53+P3,"Phi=0.6, G=2 uniform (15,15)",tau,0.8259002312520647,0.8253527513594462,0.0006628886539824756
54+P3,"Phi=0.6, G=2 uniform (15,15)",rhobar,0.44372294372294374,0.44365069827898973,0.0001628165614963565
55+P3,"Phi=0.6, G=2 uniform (15,15)",sync,0.6493659826470395,0.6495021346938776,0.00020966920115393528
56+P3,"Phi=0.6, G=3 uniform (10,10,10)",tau,0.8689607229753215,0.8685960391342109,0.00041967816435001736
57+P3,"Phi=0.6, G=3 uniform (10,10,10)",rhobar,0.4205318491032777,0.4195118784420024,0.002425430234238455
58+P3,"Phi=0.6, G=3 uniform (10,10,10)",sync,0.6401849199434013,0.6396935102040816,0.0007676059276172269
59+P3(iii),simulated tau decreasing in H within each Phi (1=yes),ordering,1.0,1.0,0.0
60+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=1, G=1 (50) (1=yes)",bounds,1.0,1.0,0.0
61+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=1, G=2 uniform (25,25) (1=yes)",bounds,1.0,1.0,0.0
62+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=1, G=5 uniform (10x5) (1=yes)",bounds,1.0,1.0,0.0
63+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=1, G=3 skewed (30,10,10) (1=yes)",bounds,1.0,1.0,0.0
64+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=0.6, G=1 (30) (1=yes)",bounds,1.0,1.0,0.0
65+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=0.6, G=2 uniform (15,15) (1=yes)",bounds,1.0,1.0,0.0
66+P3(ii),"Phi^2/G <= H <= Phi^2 for Phi=0.6, G=3 uniform (10,10,10) (1=yes)",bounds,1.0,1.0,0.0
67+P4(i),lambda*gamma=0.3,variance amplification 1/(1-lg)^2,2.0408163265306127,2.040816326530612,4.3520742565306126e-16
68+P4(iii),lambda*gamma=0.3,Var(P1-v)=sv^2[(1-tau)+tau(kappa-1)^2],0.24330132497937995,0.24370288120040037,0.0016504481471872538
69+P4(i),lambda*gamma=0.3,max|dP - kappa*lam*Y| (abs),0.0,8.881784197001252e-16,8.881784197001252e-16
70+P4(i),lambda*gamma=0.6,variance amplification 1/(1-lg)^2,6.249999999999999,6.249999999999997,2.8421709430404013e-16
71+P4(iii),lambda*gamma=0.6,Var(P1-v)=sv^2[(1-tau)+tau(kappa-1)^2],2.1586948461253246,2.160366856281589,0.00077454678657593
72+P4(i),lambda*gamma=0.6,max|dP - kappa*lam*Y| (abs),0.0,3.552713678800501e-15,3.552713678800501e-15
73+P4(ii),lambda*gamma=1.0: |dP| after 60 rounds >= 50 * |lam*Y| (1=yes),divergence,1.0,1.0,0.0
74+P4(ii),lambda*gamma=1.2: |dP| after 60 rounds >= 50 * |lam*Y| (1=yes),divergence,1.0,1.0,0.0
75+P5(ii),"a=0.60, b=0.0, dP0=-1.0",fixed point (dP0+lam*b/m)/(1-a),-2.5,-2.5,0.0
76+P5(ii),"a=0.60, b=5.0, dP0=-1.0",fixed point (dP0+lam*b/m)/(1-a),-2.25,-2.25,0.0
77+P5(ii),"a=0.60, b=0",multiplier 1/(1-a),2.5,2.5,0.0
78+P5(i),"a=0.60, b=5, dP0=-0.1 (inside buffer)",dP = dP0,-0.1,-0.1,0.0
79+P5(iii),a=1.00: cascade below -1e3 (1=yes),no finite fixed point,1.0,1.0,0.0
80+P5(iii),a=1.20: cascade below -1e3 (1=yes),no finite fixed point,1.0,1.0,0.0
81+P5(iv),phi_L=0,sd(X_net),10.788883167408942,10.758877785651544,0.00278113881592846
82+P5(iv),phi_L=0,E|X| = sigX*sqrt(2/pi),8.60828330758151,8.575270770976026,0.0038349732955942407
83+P5(iv),"phi_L=0, threshold m/lambda=10.789",P(X>=m/lambda)=2*Phibar,0.31731050786291415,0.31466,0.008353041570433076
84+P5(iv),phi_L=0.5,sd(X_net),11.679041056525145,11.63964211855805,0.0033734737104192366
85+P5(iv),phi_L=0.5,E|X| = sigX*sqrt(2/pi),9.318526543984198,9.282062585089383,0.003913060581273512
86+P5(iv),"phi_L=0.5, threshold m/lambda=10.789",P(X>=m/lambda)=2*Phibar,0.35560003994636147,0.352595,0.008450617572525456
87+P5(iv),phi_L=1,sd(X_net),14.366627996854378,14.356194803386284,0.0007262103167408725
88+P5(iv),phi_L=1,E|X| = sigX*sqrt(2/pi),11.462910669488306,11.450867751700944,0.001050598590061189
89+P5(iv),"phi_L=1, threshold m/lambda=10.789",P(X>=m/lambda)=2*Phibar,0.45267169342522484,0.4527,6.253223955967488e-05
added scripts/10_calibration_sweep.py +62 −0
@@ -0,0 +1,62 @@
1+"""Phase 3 — calibration of the classical baseline: sweep a few behavioural parameters over seeds and
2+report the stylized facts (tracking error, return autocorrelation, kurtosis, volatility clustering,
3+spread/depth, bubbles). Writes results/calibration/sweep.parquet and a summary CSV. No API calls."""
4+from __future__ import annotations
5+
6+import itertools
7+import sys
8+from concurrent.futures import ProcessPoolExecutor
9+from pathlib import Path
10+
11+import pandas as pd
12+
13+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
14+from market_agents.experiments.config import RunConfig # noqa: E402
15+from market_agents.experiments.runner import run_config # noqa: E402
16+from market_agents.metrics.outcomes import run_outcomes # noqa: E402
17+
18+ROOT = Path(__file__).resolve().parents[1]
19+GRID = {
20+ "fund_aggr": [10.0, 20.0, 40.0], # fundamentalist lots per 1 % mispricing
21+ "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 probability
23+ "mm_qty": [10, 20], # market-maker quote size
24+}
25+SEEDS = list(range(1, 11))
26+
27+
28+def make_cfg(fund_aggr, trend_share, noise_pm, mm_qty, seed):
29+ tr = trend_share * 0.6
30+ lev = trend_share * 0.4
31+ 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}})
41+
42+
43+def one(args):
44+ cfg = make_cfg(*args)
45+ return run_outcomes(run_config(cfg))
46+
47+
48+if __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())
added scripts/11_scale_benchmark.py +32 −0
@@ -0,0 +1,32 @@
1+"""Phase 3 gate — computational performance and stylized facts at N = 100, 1,000, 10,000 traders (no API)."""
2+from __future__ import annotations
3+
4+import json
5+import sys
6+import time
7+from pathlib import Path
8+
9+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
10+from market_agents.experiments.config import RunConfig # noqa: E402
11+from market_agents.experiments.runner import run_config # noqa: E402
12+from market_agents.metrics.outcomes import run_outcomes # noqa: E402
13+
14+ROOT = Path(__file__).resolve().parents[1]
15+rows = []
16+for n, seeds in ((100, [1, 2, 3]), (1000, [1, 2, 3]), (10000, [1])):
17+ for s in seeds:
18+ act = {k: v * 100.0 / n for k, v in RunConfig().population.activity.items()} # constant order flow per tick
19+ cfg = RunConfig.from_yaml(ROOT / "configs/baseline.yaml", **{"name": f"scale_{n}", "seed": s, "population.n_traders": n,
20+ "population.activity": act})
21+ t0 = time.perf_counter()
22+ res = run_config(cfg)
23+ oc = run_outcomes(res)
24+ oc["wall_s"] = time.perf_counter() - t0
25+ oc["n_orders"] = int(res.agents["n_orders"].sum())
26+ rows.append(oc)
27+ print(f"N={n} seed={s}: {oc['wall_s']:.1f}s, {oc['n_trades']} trades, {oc['n_orders']} orders, mad={oc['mad_pct']:.2f}% "
28+ f"acf1={oc['acf_r_1']:.2f} kurt={oc['kurtosis']:.1f} spread={oc['spread_bp']:.1f}bp", flush=True)
29+out = ROOT / "results/calibration"
30+out.mkdir(parents=True, exist_ok=True)
31+with open(out / "scale_benchmark.json", "w") as f:
32+ json.dump(rows, f, indent=1, default=float)
added scripts/40_figures.py +152 −0
@@ -0,0 +1,152 @@
1+"""Generate every figure of the paper from saved outcomes/run artefacts. Each figure is written to
2+results/figures/ (PDF + PNG) and copied to paper/figures/ by `make tables`. Designs that have not been run
3+are skipped with a message (no figure is ever drawn from invented numbers)."""
4+from __future__ import annotations
5+
6+import json
7+import sys
8+from pathlib import Path
9+
10+import numpy as np
11+import pandas as pd
12+
13+ROOT = Path(__file__).resolve().parents[1]
14+sys.path.insert(0, str(ROOT / "src"))
15+from market_agents.analysis.estimators import dose_response, local_projection, paired_effects # noqa: E402
16+from market_agents.analysis import plots as P # noqa: E402
17+
18+RUNS = ROOT / "results/runs"
19+OUT = ROOT / "results/figures"
20+OUT.mkdir(parents=True, exist_ok=True)
21+LABELS = {"msd_log": "squared log pricing error", "mad_pct": "mean |P/V − 1|, %", "time_to_absorb": "time to absorb shock (ticks)",
22+ "response_ratio": "20-tick response ratio", "spread_bp": "quoted spread (bp)", "eff_spread_bp": "effective spread (bp)",
23+ "depth5": "depth, 5 levels (lots)", "lambda_bp_100": "price impact (bp / 100 lots)", "n_bubbles": "bubble episodes",
24+ "bubble_amp": "bubble amplitude", "max_dd": "max drawdown", "n_crashes": "crashes", "acf_absr_1_10": "vol. clustering (Σ ACF|r|)",
25+ "kurtosis": "excess kurtosis", "vr5": "variance ratio (5)", "llm_synchrony": "LLM synchrony", "llm_pairwise_corr": "LLM order-flow corr."}
26+
27+
28+def load(design: str) -> pd.DataFrame | None:
29+ p = RUNS / design / "outcomes.parquet"
30+ if not p.exists():
31+ print(f" [skip] {design}: not run yet")
32+ return None
33+ df = pd.read_parquet(p)
34+ df["cell"] = df["name"].str.split("/").str[-1]
35+ return df
36+
37+
38+def ticks_of(run_dir: str) -> pd.DataFrame:
39+ return pd.read_parquet(Path(run_dir) / "ticks.parquet")
40+
41+
42+def fig_baseline_paths():
43+ df = load("classical_confirmatory")
44+ if df is None:
45+ return
46+ rows = [df[(df.cell == c) & (df.seed == 101)].iloc[0] for c in ("control", "leverage_heavy_funding_shock", "liquidity_shock")]
47+ fig = P.price_paths([("control", ticks_of(rows[0].run_dir)), ("leverage 19 % + funding shock", ticks_of(rows[1].run_dir)),
48+ ("market makers withdraw", ticks_of(rows[2].run_dir))], shock_t=300)
49+ P.save(fig, OUT / "fig_baseline_paths.pdf")
50+
51+
52+def fig_classical_mechanisms():
53+ df = load("classical_confirmatory")
54+ if df is None:
55+ return
56+ cells = ["control", "placebo_shock", "trend_heavy", "leverage_heavy", "leverage_heavy_funding_shock", "liquidity_shock",
57+ "info_low", "info_high", "misleading_news", "breaker_5", "poslimit_300", "auction_5"]
58+ lab = {"control": "control", "placebo_shock": "placebo", "trend_heavy": "trend 16 %", "leverage_heavy": "leverage 19 %",
59+ "leverage_heavy_funding_shock": "leverage + funding shock", "liquidity_shock": "MM withdraw", "info_low": "precision 0.5",
60+ "info_high": "precision 8", "misleading_news": "misleading news", "breaker_5": "circuit breaker 5 %", "poslimit_300": "position limit", "auction_5": "call auction"}
61+ for oc in ("mad_pct", "time_to_absorb", "max_dd", "n_bubbles", "spread_bp", "lambda_bp_100"):
62+ fig = P.cell_bars(df, oc, cells, lab, LABELS[oc], control="control")
63+ P.save(fig, OUT / f"fig_classical_{oc}.pdf")
64+
65+
66+def fig_dose_response(design: str, tag: str):
67+ df = load(design)
68+ if df is None or "tr_arm" not in df:
69+ return
70+ d = df[df.tr_arm == "dose"].copy()
71+ d["share"] = d["tr_share"].astype(float)
72+ if d["share"].nunique() < 3:
73+ return
74+ eff = {}
75+ for oc in ("msd_log", "mad_pct", "time_to_absorb", "spread_bp", "lambda_bp_100", "n_bubbles", "max_dd", "acf_absr_1_10", "kurtosis"):
76+ if oc in d:
77+ eff[oc] = dose_response(d, oc, "share", 0.0)
78+ fig = P.dose_response(eff, LABELS)
79+ P.save(fig, OUT / f"fig_dose_{tag}.pdf")
80+ # realized shares
81+ fig = P.scatter_mechanism(d, "llm_share_traders", "llm_share_volume", "share", "LLM share of trader slots", "LLM share of traded volume")
82+ P.save(fig, OUT / f"fig_realized_share_{tag}.pdf")
83+ # synchrony / correlation vs share (LLM-only cells)
84+ dd = d[d.share > 0]
85+ if "llm_pairwise_corr" in dd and dd["llm_pairwise_corr"].notna().any():
86+ fig = P.scatter_mechanism(dd, "share", "llm_pairwise_corr", "share", "LLM share", "pairwise order-flow correlation (LLM agents)")
87+ P.save(fig, OUT / f"fig_sync_{tag}.pdf")
88+ # local projections around the t = 300 shock: 25 % and 50 % vs 0 %
89+ lp = {}
90+ for lv in (0.25, 0.50, 1.00):
91+ sub = d[d.share.isin([0.0, lv])]
92+ if sub.share.nunique() < 2:
93+ continue
94+ tb = {r.run_dir: ticks_of(r.run_dir) for r in sub.itertuples()}
95+ tr = {r.run_dir: r.share for r in sub.itertuples()}
96+ lp[f"{int(lv * 100)} % LLM"] = local_projection(tb, tr, 300, H=60)
97+ if lp:
98+ P.save(P.local_projection_plot(lp), OUT / f"fig_lp_{tag}.pdf")
99+ # paths for one seed
100+ seed = int(d.seed.min())
101+ runs = [(f"{int(s * 100)} % LLM", ticks_of(d[(d.share == s) & (d.seed == seed)].iloc[0].run_dir)) for s in sorted(d.share.unique()) if ((d.share == s) & (d.seed == seed)).any()][:4]
102+ P.save(P.price_paths(runs, shock_t=300), OUT / f"fig_paths_{tag}.pdf")
103+
104+
105+def fig_arms(design: str, tag: str):
106+ df = load(design)
107+ if df is None or "tr_arm" not in df:
108+ return
109+ ref = "dose_025"
110+ cells = [c for c in ["dose_000", "dose_025", "div_prompts_025", "div_models_025", "memory_025", "channel_025", "adaptive_025",
111+ "leverage_025", "breaker_025", "poslimit_025", "fair_mini_005", "fair_latency_025"] if c in set(df.cell)]
112+ lab = {"dose_000": "0 % (control)", "dose_025": "25 % single", "div_prompts_025": "25 % diverse prompts", "div_models_025": "25 % two models",
113+ "memory_025": "25 % memory", "channel_025": "25 % channel", "adaptive_025": "25 % adaptive", "leverage_025": "25 % + leverage",
114+ "breaker_025": "25 % + breaker", "poslimit_025": "25 % + pos. limit", "fair_mini_005": "5 % mini (equal USD)", "fair_latency_025": "25 % fast latency"}
115+ for oc in ("mad_pct", "time_to_absorb", "spread_bp", "n_bubbles", "max_dd", "llm_pairwise_corr", "llm_synchrony", "llm_valid_rate"):
116+ if oc in df:
117+ P.save(P.cell_bars(df, oc, cells, lab, LABELS.get(oc, oc), control=ref), OUT / f"fig_arms_{tag}_{oc}.pdf")
118+ if {"llm_usd", "mad_pct"} <= set(df.columns):
119+ dd = df[df.cell.isin(["dose_005", "dose_025", "dose_050", "dose_100", "div_models_025", "fair_mini_005"])]
120+ P.save(P.cost_quality(dd, "llm_usd", "mad_pct", "cell", "API cost per run (USD)", LABELS["mad_pct"]), OUT / f"fig_cost_quality_{tag}.pdf")
121+
122+
123+def fig_contagion():
124+ df = load("classical_confirmatory")
125+ if df is None:
126+ return
127+ cells = [c for c in ("k2_control", "k2_leverage") if c in set(df.cell)]
128+ if cells and "excess_comovement" in df:
129+ P.save(P.cell_bars(df, "excess_comovement", cells, {"k2_control": "two assets", "k2_leverage": "two assets + leverage + funding shock"},
130+ "excess comovement (return corr − fundamental corr)"), OUT / "fig_contagion_excess.pdf")
131+ P.save(P.cell_bars(df, "spillover_dy", cells, {"k2_control": "two assets", "k2_leverage": "two assets + leverage + funding shock"},
132+ "Diebold–Yilmaz spillover (%)"), OUT / "fig_contagion_spillover.pdf")
133+
134+
135+def fig_wealth():
136+ df = load("classical_confirmatory")
137+ if df is None:
138+ return
139+ r = df[(df.cell == "control") & (df.seed == 101)].iloc[0]
140+ ag = pd.read_parquet(Path(r.run_dir) / "agents.parquet")
141+ P.save(P.wealth_and_inventory(ag, ticks_of(r.run_dir)), OUT / "fig_wealth_depth.pdf")
142+
143+
144+if __name__ == "__main__":
145+ fig_baseline_paths()
146+ fig_classical_mechanisms()
147+ fig_contagion()
148+ fig_wealth()
149+ fig_dose_response("pilot_openai", "pilot")
150+ fig_dose_response("confirmatory_openai", "conf")
151+ fig_arms("confirmatory_openai", "conf")
152+ print("figures in", OUT)
added scripts/50_tables.py +221 −0
@@ -0,0 +1,221 @@
1+"""LaTeX table fragments from saved outcomes (results/tables/*.tex). Every number comes from
2+results/runs/<design>/outcomes.parquet or the run manifests; nothing is typed by hand."""
3+from __future__ import annotations
4+
5+import json
6+import sys
7+from pathlib import Path
8+
9+import numpy as np
10+import pandas as pd
11+
12+ROOT = Path(__file__).resolve().parents[1]
13+sys.path.insert(0, str(ROOT / "src"))
14+from market_agents.analysis.estimators import dose_response, ols_seed_fe, paired_effects, holm # noqa: E402
15+
16+RUNS, OUT = ROOT / "results/runs", ROOT / "results/tables"
17+OUT.mkdir(parents=True, exist_ok=True)
18+PRIMARY = ["msd_log", "mad_pct", "time_to_absorb", "response_ratio", "spread_bp", "eff_spread_bp", "depth5", "lambda_bp_100",
19+ "n_bubbles", "bubble_amp", "max_dd", "n_crashes", "acf_absr_1_10", "kurtosis"]
20+NICE = {"msd_log": "Sq. log pricing error ($\\times 10^{4}$)", "mad_pct": "Mean $|P/V-1|$ (\\%)", "time_to_absorb": "Time to absorb (ticks)",
21+ "response_ratio": "Response ratio (20 ticks)", "spread_bp": "Quoted spread (bp)", "eff_spread_bp": "Effective spread (bp)",
22+ "depth5": "Depth, 5 levels (lots)", "lambda_bp_100": "Price impact (bp/100 lots)", "n_bubbles": "Bubble episodes",
23+ "bubble_amp": "Bubble amplitude", "max_dd": "Max drawdown", "n_crashes": "Crashes", "acf_absr_1_10": "Vol. clustering $\\sum_{1}^{10}\\mathrm{ACF}|r|$",
24+ "kurtosis": "Excess kurtosis", "vr5": "Variance ratio (5)", "acf_r_1": "ACF$_1$ of returns", "vol_bp": "Tick volatility (bp)",
25+ "llm_valid_rate": "LLM valid-decision rate", "llm_synchrony": "LLM synchrony", "llm_pairwise_corr": "LLM order-flow corr.",
26+ "llm_usd": "API cost per run (USD)", "llm_share_volume": "Realized LLM volume share", "llm_share_orders": "Realized LLM order share",
27+ "gini_final": "Wealth Gini (final)", "n_bankrupt": "Bankruptcies", "n_halts": "Halts", "excess_comovement": "Excess comovement",
28+ "spillover_dy": "DY spillover (\\%)", "ret_corr": "Return correlation", "fund_corr": "Fundamental correlation", "pnl_market_maker": "MM P\\&L (\\%)"}
29+SCALE = {"msd_log": 1e4}
30+
31+
32+def fmt(x, nd=2):
33+ if x is None or (isinstance(x, float) and not np.isfinite(x)):
34+ return "--"
35+ return f"{x:.{nd}f}"
36+
37+
38+def star(p):
39+ return "" if not np.isfinite(p) else ("$^{***}$" if p < 0.01 else "$^{**}$" if p < 0.05 else "$^{*}$" if p < 0.10 else "")
40+
41+
42+def load(design):
43+ p = RUNS / design / "outcomes.parquet"
44+ if not p.exists():
45+ print(f" [skip] {design}")
46+ return None
47+ df = pd.read_parquet(p)
48+ df["cell"] = df["name"].str.split("/").str[-1]
49+ return df
50+
51+
52+def write(name: str, body: str):
53+ (OUT / name).write_text(body)
54+ print(" wrote", name)
55+
56+
57+def tab_stylized_facts():
58+ df = load("classical_confirmatory")
59+ if df is None:
60+ return
61+ d = df[df.cell == "control"]
62+ rows = [("acf_r_1", 3), ("vr5", 2), ("kurtosis", 1), ("acf_absr_1_10", 2), ("vol_bp", 1), ("spread_bp", 1), ("eff_spread_bp", 1), ("depth5", 0),
63+ ("lambda_bp_100", 1), ("mad_pct", 2), ("n_bubbles", 2), ("max_dd", 3), ("pnl_market_maker", 1), ("gini_final", 3)]
64+ lines = ["\\begin{tabular}{lrrrr}", "\\toprule", "Statistic & Mean & SD & Min & Max \\\\", "\\midrule"]
65+ for k, nd in rows:
66+ if k in d:
67+ v = d[k].astype(float)
68+ lines.append(f"{NICE.get(k, k)} & {fmt(v.mean(), nd)} & {fmt(v.std(ddof=1), nd)} & {fmt(v.min(), nd)} & {fmt(v.max(), nd)} \\\\")
69+ lines += ["\\midrule", f"Runs (seeds) & {len(d)} & & & \\\\", "\\bottomrule", "\\end{tabular}"]
70+ write("tab_stylized_facts.tex", "\n".join(lines))
71+ # scale benchmark
72+ p = ROOT / "results/calibration/scale_benchmark.json"
73+ if p.exists():
74+ sb = pd.DataFrame(json.load(open(p)))
75+ g = sb.groupby("n_traders").agg(wall=("wall_s", "mean"), trades=("n_trades", "mean"), orders=("n_orders", "mean"), mad=("mad_pct", "mean"),
76+ acf=("acf_r_1", "mean"), kurt=("kurtosis", "mean"), spread=("spread_bp", "mean"), n=("seed", "size"))
77+ lines = ["\\begin{tabular}{rrrrrrrr}", "\\toprule", "Traders & Seeds & Wall-clock (s) & Orders & Trades & Mean $|P/V-1|$ (\\%) & ACF$_1$ & Spread (bp) \\\\", "\\midrule"]
78+ for n, r in g.iterrows():
79+ lines.append(f"{n:,} & {int(r.n)} & {fmt(r.wall, 1)} & {int(r.orders):,} & {int(r.trades):,} & {fmt(r.mad, 2)} & {fmt(r.acf, 2)} & {fmt(r.spread, 1)} \\\\")
80+ lines += ["\\bottomrule", "\\end{tabular}"]
81+ write("tab_scale.tex", "\n".join(lines))
82+
83+
84+def tab_cells(design: str, cells: list[str], labels: dict, outcomes: list[str], name: str, control: str):
85+ df = load(design)
86+ if df is None:
87+ return
88+ cells = [c for c in cells if c in set(df.cell)]
89+ outcomes = [o for o in outcomes if o in df]
90+ lines = ["\\begin{tabular}{l" + "r" * len(outcomes) + "}", "\\toprule",
91+ "Cell & " + " & ".join(NICE.get(o, o) for o in outcomes) + " \\\\", "\\midrule"]
92+ ctrl = df[df.cell == control]
93+ for c in cells:
94+ d = df[df.cell == c]
95+ vals = []
96+ for o in outcomes:
97+ sc = SCALE.get(o, 1.0)
98+ m = d[o].astype(float).mean() * sc
99+ if c == control:
100+ vals.append(fmt(m))
101+ else:
102+ e = paired_effects(df, o, "cell", c, control)
103+ vals.append(f"{fmt(m)}{star(e.get('p', np.nan))}")
104+ lines.append(f"{labels.get(c, c)} & " + " & ".join(vals) + " \\\\")
105+ lines += ["\\midrule", f"Seeds per cell & {len(ctrl)}" + " &" * (len(outcomes) - 1) + " \\\\", "\\bottomrule", "\\end{tabular}"]
106+ write(name, "\n".join(lines))
107+
108+
109+def tab_effects(design: str, cells: list[str], labels: dict, outcomes: list[str], name: str, control: str, holm_family=True):
110+ """Paired effects vs control with 95 % CI and Holm-adjusted p within the primary family."""
111+ df = load(design)
112+ if df is None:
113+ return
114+ cells = [c for c in cells if c in set(df.cell) and c != control]
115+ outcomes = [o for o in outcomes if o in df]
116+ rows = []
117+ for c in cells:
118+ for o in outcomes:
119+ e = paired_effects(df, o, "cell", c, control)
120+ e["cell"] = c
121+ rows.append(e)
122+ t = pd.DataFrame(rows)
123+ if "p" not in t:
124+ return
125+ t["p_holm"] = np.nan
126+ for c in cells:
127+ m = t.cell == c
128+ t.loc[m, "p_holm"] = holm(t.loc[m, "p"].to_numpy())
129+ t.to_csv(OUT / name.replace(".tex", ".csv"), index=False)
130+ lines = ["\\begin{tabular}{llrrrrr}", "\\toprule", "Cell & Outcome & Control & Effect & 95\\% CI & $d$ & $p$ (Holm) \\\\", "\\midrule"]
131+ for c in cells:
132+ for i, o in enumerate(outcomes):
133+ r = t[(t.cell == c) & (t.outcome == o)].iloc[0]
134+ sc = SCALE.get(o, 1.0)
135+ lab = labels.get(c, c) if i == 0 else ""
136+ lines.append(f"{lab} & {NICE.get(o, o)} & {fmt(r.control_mean * sc)} & {fmt(r['diff'] * sc)}{star(r.p_holm)} & "
137+ f"[{fmt(r.ci_lo * sc)}, {fmt(r.ci_hi * sc)}] & {fmt(r.cohen_d)} & {fmt(r.p_holm, 3)} \\\\")
138+ lines.append("\\addlinespace")
139+ lines += ["\\bottomrule", "\\end{tabular}"]
140+ write(name, "\n".join(lines))
141+
142+
143+def tab_dose(design: str, name: str):
144+ df = load(design)
145+ if df is None or "tr_arm" not in df:
146+ return
147+ d = df[df.tr_arm == "dose"].copy()
148+ d["share"] = d.tr_share.astype(float)
149+ outs = [o for o in ("msd_log", "mad_pct", "time_to_absorb", "spread_bp", "lambda_bp_100", "n_bubbles", "max_dd", "acf_absr_1_10", "llm_pairwise_corr", "llm_valid_rate") if o in d]
150+ levels = sorted(d.share.unique())
151+ lines = ["\\begin{tabular}{l" + "r" * len(levels) + "}", "\\toprule", "Outcome & " + " & ".join(f"{int(l * 100)}\\%" for l in levels) + " \\\\", "\\midrule"]
152+ for o in outs:
153+ sc = SCALE.get(o, 1.0)
154+ dr = dose_response(d, o, "share", 0.0).set_index("level") if o not in ("llm_pairwise_corr", "llm_valid_rate") else None
155+ cells = []
156+ for l in levels:
157+ m = d[d.share == l][o].astype(float).mean() * sc
158+ s = "" if (dr is None or l == 0.0 or l not in dr.index) else star(dr.loc[l, "p_holm"])
159+ cells.append(f"{fmt(m)}{s}")
160+ lines.append(f"{NICE.get(o, o)} & " + " & ".join(cells) + " \\\\")
161+ rs = d.groupby("share")[["llm_share_traders", "llm_share_orders", "llm_share_volume", "llm_usd"]].mean()
162+ lines.append("\\midrule")
163+ for k, lab in (("llm_share_traders", "Realized share of traders"), ("llm_share_orders", "Realized share of orders"), ("llm_share_volume", "Realized share of volume"), ("llm_usd", "API cost per run (USD)")):
164+ lines.append(f"{lab} & " + " & ".join(fmt(rs.loc[l, k], 3) for l in levels) + " \\\\")
165+ lines += ["\\midrule", f"Seeds per cell & {int(d.groupby('share').size().min())}" + " &" * (len(levels) - 1) + " \\\\", "\\bottomrule", "\\end{tabular}"]
166+ write(name, "\n".join(lines))
167+
168+
169+def tab_llm_reliability(design: str, name: str):
170+ df = load(design)
171+ if df is None:
172+ return
173+ rows = []
174+ for c, d in df[df.llm_share > 0].groupby("cell"):
175+ rec = []
176+ for rd in d.run_dir:
177+ p = Path(rd) / "llm_decisions.parquet"
178+ if p.exists():
179+ rec.append(pd.read_parquet(p))
180+ if not rec:
181+ continue
182+ L = pd.concat(rec)
183+ rows.append({"cell": c, "n": len(L), "valid": L.valid.mean(), "hold": (L.action == "hold").mean(), "place": (L.action == "place").mean(),
184+ "market": (L.order_type == "market").mean(), "json_err": L.reason.str.startswith("json").mean(), "schema_err": L.reason.str.startswith("schema").mean(),
185+ "feas_err": L.reason.str.startswith("place").mean(), "api_err": (L.error != "").mean(), "lat": L.latency_s.mean(),
186+ "in_tok": L.input_tokens.mean(), "out_tok": L.output_tokens.mean(), "reason_tok": L.reasoning_tokens.mean(), "usd": L.usd.sum() / len(d)})
187+ if not rows:
188+ return
189+ t = pd.DataFrame(rows)
190+ t.to_csv(OUT / name.replace(".tex", ".csv"), index=False)
191+ lines = ["\\begin{tabular}{lrrrrrrrrrr}", "\\toprule", "Cell & Decisions & Valid & Hold & Market & JSON err. & Schema err. & Infeasible & API err. & Latency (s) & USD/run \\\\", "\\midrule"]
192+ for r in t.itertuples():
193+ lines.append(f"{r.cell.replace('_', '\\_')} & {r.n:,} & {fmt(r.valid, 3)} & {fmt(r.hold, 2)} & {fmt(r.market, 2)} & {fmt(r.json_err, 3)} & {fmt(r.schema_err, 3)} & {fmt(r.feas_err, 3)} & {fmt(r.api_err, 3)} & {fmt(r.lat, 2)} & {fmt(r.usd, 2)} \\\\")
194+ lines += ["\\bottomrule", "\\end{tabular}"]
195+ write(name, "\n".join(lines))
196+
197+
198+CLASSICAL_CELLS = ["control", "placebo_shock", "trend_heavy", "leverage_heavy", "leverage_heavy_funding_shock", "liquidity_shock", "info_low", "info_high",
199+ "misleading_news", "breaker_5", "poslimit_300", "auction_5", "mock_llm_25_indep", "mock_llm_50_indep"]
200+CLASSICAL_LAB = {"control": "Control", "placebo_shock": "Placebo shock", "trend_heavy": "Trend followers 16\\%", "leverage_heavy": "Leveraged 19\\%",
201+ "leverage_heavy_funding_shock": "Leveraged 19\\% + funding shock", "liquidity_shock": "Market makers withdraw", "info_low": "Signal precision 0.5",
202+ "info_high": "Signal precision 8", "misleading_news": "Misleading news 50\\%", "breaker_5": "Circuit breaker 5\\%", "poslimit_300": "Position limit 300",
203+ "auction_5": "Call auction (5 ticks)", "mock_llm_25_indep": "Mock LLM 25\\%", "mock_llm_50_indep": "Mock LLM 50\\%"}
204+ARM_CELLS = ["dose_000", "dose_025", "div_prompts_025", "div_models_025", "memory_025", "channel_025", "adaptive_025", "leverage_025", "breaker_025", "poslimit_025", "fair_mini_005", "fair_latency_025"]
205+ARM_LAB = {"dose_000": "0\\% (control)", "dose_025": "25\\% single model/prompt", "div_prompts_025": "25\\% diverse prompts", "div_models_025": "25\\% two models",
206+ "memory_025": "25\\% memory (5)", "channel_025": "25\\% communication channel", "adaptive_025": "25\\% adaptive feedback", "leverage_025": "25\\% + leveraged 19\\%",
207+ "breaker_025": "25\\% + circuit breaker", "poslimit_025": "25\\% + position limit", "fair_mini_005": "5\\% gpt-5.4-mini (equal USD)", "fair_latency_025": "25\\% matched fast latency"}
208+
209+if __name__ == "__main__":
210+ tab_stylized_facts()
211+ tab_cells("classical_confirmatory", CLASSICAL_CELLS, CLASSICAL_LAB, ["mad_pct", "time_to_absorb", "spread_bp", "lambda_bp_100", "n_bubbles", "max_dd", "n_crashes", "acf_absr_1_10"], "tab_classical_cells.tex", "control")
212+ tab_effects("classical_confirmatory", CLASSICAL_CELLS, CLASSICAL_LAB, ["mad_pct", "time_to_absorb", "spread_bp", "n_bubbles", "max_dd"], "tab_classical_effects.tex", "control")
213+ tab_cells("classical_confirmatory", ["k2_control", "k2_leverage"], {"k2_control": "Two assets", "k2_leverage": "Two assets + leveraged 19\\% + funding shock"},
214+ ["ret_corr", "fund_corr", "excess_comovement", "spillover_dy", "mad_pct", "max_dd"], "tab_contagion.tex", "k2_control")
215+ tab_dose("pilot_openai", "tab_dose_pilot.tex")
216+ tab_dose("confirmatory_openai", "tab_dose_conf.tex")
217+ tab_cells("confirmatory_openai", ARM_CELLS, ARM_LAB, ["mad_pct", "time_to_absorb", "spread_bp", "n_bubbles", "max_dd", "llm_pairwise_corr", "llm_synchrony", "llm_valid_rate"], "tab_arms_conf.tex", "dose_025")
218+ tab_effects("confirmatory_openai", ARM_CELLS, ARM_LAB, ["mad_pct", "time_to_absorb", "spread_bp", "n_bubbles", "max_dd"], "tab_arms_effects.tex", "dose_025")
219+ tab_llm_reliability("pilot_openai", "tab_llm_reliability_pilot.tex")
220+ tab_llm_reliability("confirmatory_openai", "tab_llm_reliability_conf.tex")
221+ print("tables in", OUT)
added scripts/90_theory_checks.py +264 −0
@@ -0,0 +1,264 @@
1+#!/usr/bin/env python
2+"""Numerical verification of the closed forms of Propositions 1-5 (paper/sections/theory.tex).
3+
4+Monte Carlo with N = 50 traders and R = 200,000 draws of the one-period model
5+ s_i = v + eta_{g(i)} + u_i, x_i = beta s_i, Y = sum_i x_i + z, P = lambda Y.
6+Every row of results/tables/theory_checks.csv compares a closed form with its simulated
7+counterpart and reports the relative error; the script exits with a non-zero code if any
8+relative error exceeds 2 %.
9+
10+Run: .venv/bin/python scripts/90_theory_checks.py
11+"""
12+from __future__ import annotations
13+
14+import math
15+import sys
16+from pathlib import Path
17+
18+import numpy as np
19+import pandas as pd
20+
21+ROOT = Path(__file__).resolve().parents[1]
22+OUT = ROOT / "results" / "tables" / "theory_checks.csv"
23+
24+# ----------------------------------------------------------------------------- primitives
25+N = 50 # traders
26+R = 200_000 # Monte Carlo draws
27+SV, SE, SU, SZ = 1.0, 1.0, 0.8, 5.0 # sigma_v, sigma_eta, sigma_u, sigma_z
28+BETA = 1.0
29+TOL = 0.02
30+rng = np.random.default_rng(20261027)
31+
32+SS2 = SV**2 + SE**2 + SU**2 # signal variance sigma_s^2
33+RHO0 = SV**2 / SS2 # different / private models
34+RHO1 = (SV**2 + SE**2) / SS2 # same model
35+
36+rows: list[dict] = []
37+
38+
39+def add(prop: str, case: str, quantity: str, theory: float, sim: float) -> None:
40+ rel = abs(sim - theory) / abs(theory) if theory != 0 else abs(sim - theory)
41+ rows.append(dict(proposition=prop, case=case, quantity=quantity, theory=theory,
42+ simulation=sim, rel_error=rel))
43+
44+
45+# ----------------------------------------------------------------------------- closed forms
46+def omega(H: float, Phi: float) -> float:
47+ return SE**2 * (H + (1.0 - Phi) / N) + SU**2 / N + SZ**2 / (BETA**2 * N**2)
48+
49+
50+def closed_forms(sizes: list[int]) -> dict:
51+ Phi = sum(sizes) / N
52+ H = sum((n / N) ** 2 for n in sizes)
53+ Om = omega(H, Phi)
54+ tau = SV**2 / (SV**2 + Om)
55+ lam = tau / (BETA * N)
56+ w = (N * H - Phi) / (N - 1) # share of same-model ordered pairs
57+ rhobar = RHO0 + w * SE**2 / SS2
58+ sync = 0.5 + ((1 - w) * math.asin(RHO0) + w * math.asin(RHO1)) / math.pi
59+ return dict(H=H, Phi=Phi, Omega=Om, tau=tau, lam=lam, w=w, rhobar=rhobar, sync=sync,
60+ mse=SV**2 * (1 - tau))
61+
62+
63+# ----------------------------------------------------------------------------- simulation
64+def simulate(sizes: list[int]) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
65+ """Draw (v, x, Y) for a population with shared-model cohorts of the given sizes
66+ (first cohort occupies traders 0..n_1-1, etc.); remaining traders use private models."""
67+ v = rng.normal(0.0, SV, R)
68+ eps = rng.normal(0.0, SU, (R, N))
69+ start = 0
70+ for n in sizes:
71+ eps[:, start:start + n] += rng.normal(0.0, SE, (R, 1))
72+ start += n
73+ if start < N:
74+ eps[:, start:] += rng.normal(0.0, SE, (R, N - start))
75+ x = BETA * (v[:, None] + eps)
76+ Y = x.sum(axis=1) + rng.normal(0.0, SZ, R)
77+ return v, x, Y
78+
79+
80+def flow_stats(v: np.ndarray, x: np.ndarray, Y: np.ndarray) -> dict:
81+ cov = np.cov(v, Y, ddof=0)
82+ lam_hat = cov[0, 1] / cov[1, 1]
83+ tau_hat = cov[0, 1] ** 2 / (cov[0, 0] * cov[1, 1])
84+ P = lam_hat * Y
85+ mse = np.var(P - v)
86+ C = np.corrcoef(x, rowvar=False)
87+ off = ~np.eye(N, dtype=bool)
88+ rhobar = C[off].mean()
89+ sg = np.sign(x)
90+ A = (sg.T @ sg) / R # E[sign_i sign_j]
91+ sync = ((1.0 + A[off]) / 2.0).mean()
92+ return dict(tau=tau_hat, lam=lam_hat, mse=mse, rhobar=rhobar, sync=sync)
93+
94+
95+def check_population(prop: str, label: str, sizes: list[int], what=("tau", "lam", "mse", "rhobar", "sync")):
96+ th = closed_forms(sizes)
97+ v, x, Y = simulate(sizes)
98+ sim = flow_stats(v, x, Y)
99+ for q in what:
100+ add(prop, label, q, th[q], sim[q])
101+ return th, sim, (v, x, Y)
102+
103+
104+# ============================================================================= Props 1-2
105+print(f"Monte Carlo: N={N}, R={R:,}, sigma_v={SV}, sigma_eta={SE}, sigma_u={SU}, sigma_z={SZ}, beta={BETA}")
106+single_cases = {"phi=0": [], "phi=0.1": [5], "phi=0.2": [10], "phi=0.5": [25], "phi=1": [50]}
107+taus = {}
108+keep = None
109+for label, sizes in single_cases.items():
110+ th, sim, draws = check_population("P1-P2", f"single model, {label}", sizes)
111+ taus[label] = (th["tau"], sim["tau"])
112+ if label == "phi=0.2":
113+ keep = draws
114+
115+# Prop 1(i): monotonicity across the grid (theory) and in simulation
116+tau_th = [taus[k][0] for k in single_cases]
117+tau_mc = [taus[k][1] for k in single_cases]
118+add("P1(i)", "tau strictly decreasing over phi grid (1=yes)", "monotone",
119+ 1.0, float(all(np.diff(tau_th) < 0) and all(np.diff(tau_mc) < 0)))
120+# Prop 1(i): phi=0 and phi=1/N coincide in Omega
121+add("P1(i)", "Omega(phi=1/N) / Omega(phi=0)", "ratio", 1.0, omega((1 / N) ** 2, 1 / N) / omega(0.0, 0.0))
122+# Prop 1(ii): uniform bound tau(phi) <= tau_inf(phi) and N->infinity limit (theory, large N)
123+for phi in (0.2, 0.5):
124+ tau_inf = SV**2 / (SV**2 + phi**2 * SE**2)
125+ OmN = lambda n: SE**2 * (phi**2 + (1 - phi) / n) + SU**2 / n + SZ**2 / (BETA**2 * n**2)
126+ tau_big = SV**2 / (SV**2 + OmN(10**7))
127+ add("P1(ii)", f"N->inf limit, phi={phi}", "tau_inf", tau_inf, tau_big)
128+ add("P1(ii)", f"bound tau(N=50)<=tau_inf, phi={phi} (1=yes)", "bound", 1.0,
129+ float(SV**2 / (SV**2 + OmN(N)) <= tau_inf))
130+add("P1(iii)", "phi=0, N=1e7", "tau->1", 1.0,
131+ SV**2 / (SV**2 + (SE**2 + SU**2) / 1e7 + SZ**2 / (BETA**2 * 1e14)))
132+
133+# Prop 2(ii): same-model vs cross-model pair correlations in the phi=0.5 population
134+v, x, Y = simulate([25])
135+C = np.corrcoef(x, rowvar=False)
136+same = C[:25, :25][~np.eye(25, dtype=bool)].mean()
137+cross = C[:25, 25:].mean()
138+add("P2(ii)", "phi=0.5", "rho_same", RHO1, same)
139+add("P2(ii)", "phi=0.5", "rho_cross", RHO0, cross)
140+add("P2(iv)", "phi=0.5, same-model pair", "S(rho1)=1/2+asin(rho1)/pi", 0.5 + math.asin(RHO1) / math.pi,
141+ ((1 + (np.sign(x[:, :25]).T @ np.sign(x[:, :25]) / R)[~np.eye(25, dtype=bool)]) / 2).mean())
142+# Prop 2 placebo: sigma_eta = 0 -> no dependence on phi (theory)
143+add("P2 placebo", "sigma_eta=0: Omega(phi=1)/Omega(phi=0)", "ratio", 1.0,
144+ (SU**2 / N + SZ**2 / (BETA**2 * N**2)) / (SU**2 / N + SZ**2 / (BETA**2 * N**2)))
145+del v, x, Y, C
146+
147+# ============================================================================= Prop 3
148+div_cases = {
149+ "Phi=1, G=1 (50)": [50],
150+ "Phi=1, G=2 uniform (25,25)": [25, 25],
151+ "Phi=1, G=5 uniform (10x5)": [10] * 5,
152+ "Phi=1, G=3 skewed (30,10,10)": [30, 10, 10],
153+ "Phi=0.6, G=1 (30)": [30],
154+ "Phi=0.6, G=2 uniform (15,15)": [15, 15],
155+ "Phi=0.6, G=3 uniform (10,10,10)": [10, 10, 10],
156+}
157+div_tau = {}
158+for label, sizes in div_cases.items():
159+ th, sim, _ = check_population("P3", label, sizes, what=("tau", "rhobar", "sync"))
160+ div_tau[label] = (th["H"], th["tau"], sim["tau"])
161+# simulated tau is decreasing in H within each Phi group (hence single <= mixed <= uniform)
162+ok = True
163+for Phi_label in ("Phi=1", "Phi=0.6"):
164+ grp = sorted((H, tau_s) for lbl, (H, _, tau_s) in div_tau.items() if lbl.startswith(Phi_label + ","))
165+ ok = ok and all(grp[k][1] > grp[k + 1][1] for k in range(len(grp) - 1)) # H ascending -> tau descending
166+add("P3(iii)", "simulated tau decreasing in H within each Phi (1=yes)", "ordering", 1.0, float(ok))
167+# H bounds
168+for label, sizes in div_cases.items():
169+ H, Phi, G = sum((n / N) ** 2 for n in sizes), sum(sizes) / N, len(sizes)
170+ add("P3(ii)", f"Phi^2/G <= H <= Phi^2 for {label} (1=yes)", "bounds", 1.0,
171+ float(Phi**2 / G - 1e-12 <= H <= Phi**2 + 1e-12))
172+
173+# ============================================================================= Prop 4
174+v, x, Y = keep
175+th = closed_forms([10])
176+lam = th["lam"]
177+for lg in (0.3, 0.6):
178+ gamma = lg / lam
179+ dP = lam * Y.copy() # start of the iterated response
180+ for _ in range(400):
181+ dP = lam * (Y + gamma * dP)
182+ amp = np.var(dP) / np.var(lam * Y)
183+ add("P4(i)", f"lambda*gamma={lg}", "variance amplification 1/(1-lg)^2", 1 / (1 - lg) ** 2, amp)
184+ kappa = 1 / (1 - lg)
185+ add("P4(iii)", f"lambda*gamma={lg}", "Var(P1-v)=sv^2[(1-tau)+tau(kappa-1)^2]",
186+ SV**2 * ((1 - th["tau"]) + th["tau"] * (kappa - 1) ** 2), np.var(dP - v))
187+ # fixed point = kappa*lambda*Y
188+ add("P4(i)", f"lambda*gamma={lg}", "max|dP - kappa*lam*Y| (abs)", 0.0,
189+ float(np.max(np.abs(dP - kappa * lam * Y))))
190+# divergence when lambda*gamma >= 1
191+for lg in (1.0, 1.2):
192+ gamma = lg / lam
193+ dP = lam * Y[:1000].copy()
194+ for _ in range(60):
195+ dP = lam * (Y[:1000] + gamma * dP)
196+ # lambda*gamma = 1 diverges linearly (|dP| = 61|lam Y| after 60 rounds), > 1 geometrically
197+ add("P4(ii)", f"lambda*gamma={lg}: |dP| after 60 rounds >= 50 * |lam*Y| (1=yes)", "divergence",
198+ 1.0, float(np.all(np.abs(dP) >= 50 * np.abs(lam * Y[:1000]))))
199+del v, x, Y
200+
201+# ============================================================================= Prop 5
202+# (ii)-(iii): cascade fixed point and multiplier
203+def cascade(a: float, dP0: float, b: float, X: float, lam_: float, m: float, rounds: int = 2000) -> float:
204+ dP = dP0
205+ for _ in range(rounds):
206+ F = max(0.0, (-X * dP - b) / m)
207+ dP = dP0 - lam_ * F
208+ if dP < -1e6:
209+ return dP
210+ return dP
211+
212+
213+lam_, m, X = 0.02, 1.0, 30.0 # a = 0.6
214+a = lam_ * X / m
215+for b in (0.0, 5.0):
216+ dP0 = -1.0
217+ fp = cascade(a, dP0, b, X, lam_, m)
218+ add("P5(ii)", f"a={a:.2f}, b={b}, dP0={dP0}", "fixed point (dP0+lam*b/m)/(1-a)",
219+ (dP0 + lam_ * b / m) / (1 - a), fp)
220+add("P5(ii)", f"a={a:.2f}, b=0", "multiplier 1/(1-a)", 1 / (1 - a), cascade(a, -1.0, 0.0, X, lam_, m) / -1.0)
221+add("P5(i)", f"a={a:.2f}, b=5, dP0=-0.1 (inside buffer)", "dP = dP0", -0.1, cascade(a, -0.1, 5.0, X, lam_, m))
222+for Xs in (50.0, 60.0): # a = 1.0, 1.2
223+ fp = cascade(lam_ * Xs / m, -1.0, 0.0, Xs, lam_, m, rounds=2000) # a=1: linear, -2001 after 2000 rounds
224+ add("P5(iii)", f"a={lam_ * Xs / m:.2f}: cascade below -1e3 (1=yes)", "no finite fixed point", 1.0,
225+ float(fp < -1e3))
226+
227+# (iv): distribution of the cohort net position X = |sum_{i in L} x_i|, N_L = 10 leveraged traders
228+NL = 10
229+c_thr = None
230+for phiL, sizes in (("phi_L=0", []), ("phi_L=0.5", [5]), ("phi_L=1", [10])):
231+ v = rng.normal(0.0, SV, R)
232+ eps = rng.normal(0.0, SU, (R, NL))
233+ if sizes:
234+ eps[:, :sizes[0]] += rng.normal(0.0, SE, (R, 1))
235+ eps[:, sizes[0]:] += rng.normal(0.0, SE, (R, NL - sizes[0]))
236+ else:
237+ eps += rng.normal(0.0, SE, (R, NL))
238+ xL = BETA * (v[:, None] + eps)
239+ Xnet = np.abs(xL.sum(axis=1))
240+ nL = sizes[0] if sizes else 0
241+ wL = nL * (nL - 1) / (NL * (NL - 1))
242+ rhoL = RHO0 + wL * SE**2 / SS2
243+ sigX = BETA * math.sqrt(SS2) * math.sqrt(NL * (1 + (NL - 1) * rhoL))
244+ if c_thr is None:
245+ c_thr = 1.0 * sigX # fixed spiral threshold m/lambda across cases
246+ add("P5(iv)", phiL, "sd(X_net)", sigX, float(np.std(xL.sum(axis=1))))
247+ add("P5(iv)", phiL, "E|X| = sigX*sqrt(2/pi)", sigX * math.sqrt(2 / math.pi), float(Xnet.mean()))
248+ p_th = 2 * (1 - 0.5 * (1 + math.erf(c_thr / sigX / math.sqrt(2))))
249+ add("P5(iv)", f"{phiL}, threshold m/lambda={c_thr:.3f}", "P(X>=m/lambda)=2*Phibar", p_th,
250+ float((Xnet >= c_thr).mean()))
251+ del v, eps, xL, Xnet
252+
253+# ============================================================================= report
254+df = pd.DataFrame(rows)
255+OUT.parent.mkdir(parents=True, exist_ok=True)
256+df.to_csv(OUT, index=False)
257+pd.set_option("display.width", 200, "display.max_rows", 500, "display.max_colwidth", 60)
258+print(df.to_string(index=False, float_format=lambda f: f"{f:.5f}"))
259+worst = df["rel_error"].max()
260+print(f"\n{len(df)} checks written to {OUT.relative_to(ROOT)}; max relative error = {worst:.4%}")
261+if worst >= TOL:
262+ print(f"FAIL: at least one relative error >= {TOL:.0%}")
263+ sys.exit(1)
264+print(f"OK: all relative errors < {TOL:.0%}")
added scripts/analyze.py +6 −0
@@ -0,0 +1,6 @@
1+"""Run the full analysis: figures + tables (same as `make figures tables`)."""
2+import runpy, sys
3+from pathlib import Path
4+here = Path(__file__).resolve().parent
5+runpy.run_path(str(here / "40_figures.py"), run_name="__main__")
6+runpy.run_path(str(here / "50_tables.py"), run_name="__main__")
added scripts/run_experiment.py +6 −0
@@ -0,0 +1,6 @@
1+"""Thin wrapper: `python scripts/run_experiment.py configs/<design>.yaml [--workers N] [--cache path]` (same as `ma design`)."""
2+import sys
3+from pathlib import Path
4+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
5+from market_agents.cli import main # noqa: E402
6+main(["design", *sys.argv[1:]])
added scripts/smoke_test.py +22 −0
@@ -0,0 +1,22 @@
1+"""Minimal end-to-end experiment: 100 agents, fixed seed, zero paid calls (mock LLM at 25 %), accounting verified."""
2+from __future__ import annotations
3+
4+import sys
5+from pathlib import Path
6+
7+ROOT = Path(__file__).resolve().parents[1]
8+sys.path.insert(0, str(ROOT / "src"))
9+from market_agents.experiments.config import RunConfig # noqa: E402
10+from market_agents.experiments.runner import run_config # noqa: E402
11+from market_agents.metrics.outcomes import run_outcomes # noqa: E402
12+
13+if __name__ == "__main__":
14+ out = ROOT / "results/runs/smoke"
15+ for share in (0.0, 0.25):
16+ cfg = RunConfig.from_yaml(ROOT / "configs/baseline.yaml", **{"name": f"smoke_{int(share * 100)}", "seed": 1, "n_ticks": 300,
17+ "population.llm_share": share, "population.llm.provider_mix": {"mock": 1.0}})
18+ res = run_config(cfg, out_dir=out / cfg.name)
19+ oc = run_outcomes(res)
20+ print(f"{cfg.name}: trades={oc['n_trades']} mad={oc['mad_pct']:.2f}% spread={oc['spread_bp']:.1f}bp bubbles={oc['n_bubbles']} "
21+ f"llm_decisions={oc['n_llm_decisions']} valid={oc.get('llm_valid_rate', float('nan')):.2f} conservation_ok={res.manifest['final_conservation_ok']} wall={oc['wall_s']:.2f}s")
22+ print("smoke test OK — artefacts in", out)
added src/market_agents/__init__.py +2 −0
@@ -0,0 +1,2 @@
1+"""market_agents — event-sourced LOB simulator with classical and LLM agent populations (UQO WP 27)."""
2+__version__ = "0.1.0"
added src/market_agents/agents/__init__.py +0 −0
added src/market_agents/agents/base.py +98 −0
@@ -0,0 +1,98 @@
1+"""Agent interface: a Policy maps a canonical Observation to an Action.
2+
3+The Observation is the *only* channel from the market to any policy (classical or LLM); it carries
4+no latent state and no future information. The same object is serialized into the LLM prompt, so
5+matched conditions see literally the same information.
6+"""
7+from __future__ import annotations
8+
9+from dataclasses import dataclass, field
10+from typing import Protocol
11+
12+import numpy as np
13+
14+from ..exchange.orders import Action
15+
16+
17+@dataclass(slots=True)
18+class BookView:
19+ best_bid: int | None
20+ best_ask: int | None
21+ bids: list[tuple[int, int]] # (price_ticks, qty) best first
22+ asks: list[tuple[int, int]]
23+ mid: float
24+ last_price: int | None
25+ halted: bool
26+
27+
28+@dataclass(slots=True)
29+class OwnOrderView:
30+ order_id: int
31+ asset: int
32+ side: int
33+ qty: int
34+ remaining: int
35+ price: int | None
36+ age: float
37+
38+
39+@dataclass(slots=True)
40+class Observation:
41+ t: int
42+ agent_id: int
43+ books: dict[int, BookView]
44+ mid_history: dict[int, np.ndarray] # last `lookback` mids per asset (ticks)
45+ recent_trades: dict[int, list[tuple[float, int, int, int]]] # (time, price, qty, aggressor)
46+ last_dividend: dict[int, float] # cents per lot, last paid
47+ public_value: dict[int, float] # cents per lot: PV under public information (announced dividends + news)
48+ x_hat_public: dict[int, float] # public posterior mean of the upcoming dividend deviation (cents)
49+ news: list # NewsItem list visible in window
50+ private_signals: dict[int, float] # asset -> signal about x_t (only for informed agents)
51+ signal_precision: dict[int, float]
52+ cash: int
53+ free_cash: int
54+ positions: dict[int, int]
55+ equity: float
56+ open_orders: list[OwnOrderView]
57+ leverage_cap: float
58+ short_allowed: bool
59+ max_order_qty: int
60+ tick_cents: dict[int, int]
61+ div_interval: int
62+ ticks_to_dividend: int
63+ r_period: float
64+ memory: list[str] = field(default_factory=list) # bounded memory (LLM agents)
65+ channel: list[str] = field(default_factory=list) # cohort messages (communication treatment)
66+ realized_pnl_history: list[float] = field(default_factory=list)
67+
68+
69+class Policy(Protocol):
70+ name: str
71+
72+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action: ...
73+
74+
75+@dataclass(slots=True)
76+class AgentSpec:
77+ """Static description of one trader slot (assigned at population build time)."""
78+ agent_id: int
79+ policy_name: str
80+ policy_kwargs: dict
81+ cash: int
82+ positions: dict[int, int]
83+ latency: float
84+ activity: float # per-tick probability of being activated (classical)
85+ decision_interval: int = 1 # LLM agents: decide every `decision_interval` ticks
86+ decision_offset: int = 0
87+ signal_precision: float = 0.0 # 0 => uninformed
88+ leverage_cap: float = 1.0
89+ short_allowed: bool = False
90+ stratum: str = "" # wealth/information/latency stratum used for randomization
91+ provider: str = "" # LLM provider label ('' for classical)
92+ model: str = ""
93+ prompt_id: str = ""
94+ cohort: int = -1 # communication cohort (-1: none)
95+
96+
97+def clip_qty(q: float, max_q: int) -> int:
98+ return int(max(0, min(max_q, round(q))))
added src/market_agents/agents/classical.py +338 −0
@@ -0,0 +1,338 @@
1+"""Classical (non-LLM) trading policies. Each is a small, documented heuristic with explicit
2+parameters; none sees the latent fundamental, only the Observation.
3+
4+Price arithmetic is in ticks; quantities in lots. Every policy returns at most a handful of intents.
5+"""
6+from __future__ import annotations
7+
8+from dataclasses import dataclass
9+
10+import numpy as np
11+
12+from ..exchange.orders import Action, CancelIntent, OrderIntent, OrderType, Side
13+from .base import Observation, clip_qty
14+
15+
16+def _limit_or_market(rng: np.random.Generator, p_market: float) -> OrderType:
17+ return OrderType.MARKET if rng.random() < p_market else OrderType.LIMIT
18+
19+
20+def _cancel_all(obs: Observation, asset: int | None = None) -> list[CancelIntent]:
21+ return [CancelIntent(o.order_id) for o in obs.open_orders if asset is None or o.asset == asset]
22+
23+
24+def _affordable(obs: Observation, price_ticks: float, tick_cents: int) -> int:
25+ return int(max(0, obs.free_cash // max(1, int(price_ticks * tick_cents * 1.001))))
26+
27+
28+@dataclass
29+class Fundamentalist:
30+ """Value trader. Posterior value = public PV adjusted by the private signal (if informed):
31+ V_hat = public_value + w * (signal - x_hat_public) * rho/(1+r-rho) with w = prec/(1+prec).
32+ Trades toward the value when the relative mispricing exceeds `threshold`; order size scales with
33+ the mispricing (capped) and with available cash/shares. The signal adjustment is relative to the
34+ public posterior mean x_hat (shown in the Observation), scaled by 1/(1+r-rho)."""
35+ name: str = "fundamentalist"
36+ threshold: float = 0.005
37+ aggressiveness: float = 20.0 # lots per 1% mispricing
38+ p_market: float = 0.3
39+ max_qty: int = 50
40+ improve_ticks: int = 1 # limit orders are placed inside the spread by this many ticks
41+ cross_mult: float = 2.0 # beyond cross_mult*threshold the limit order is marketable (at the far quote)
42+
43+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
44+ act = Action()
45+ for a, book in obs.books.items():
46+ if book.halted:
47+ continue
48+ tc = obs.tick_cents[a]
49+ v_pub = obs.public_value[a] / tc # ticks
50+ v_hat = v_pub
51+ if a in obs.private_signals:
52+ prec = obs.signal_precision.get(a, 0.0)
53+ w = prec / (1.0 + prec)
54+ rho = 0.98
55+ # posterior mean of x_n combines the public prior (in v_pub) with the private signal, weight w
56+ v_hat = v_pub + w * (obs.private_signals[a] - obs.x_hat_public.get(a, 0.0)) / (1.0 + obs.r_period - rho) / tc
57+ mid = book.mid
58+ if mid <= 0:
59+ continue
60+ m = (v_hat - mid) / mid
61+ if abs(m) < self.threshold:
62+ continue
63+ q = clip_qty(abs(m) * 100.0 * self.aggressiveness, min(self.max_qty, obs.max_order_qty))
64+ if q == 0:
65+ continue
66+ act.cancels += _cancel_all(obs, a)
67+ otype = _limit_or_market(rng, self.p_market)
68+ if m > 0:
69+ q = min(q, _affordable(obs, book.best_ask or mid, tc))
70+ if q <= 0:
71+ continue
72+ if otype is OrderType.MARKET:
73+ price = None
74+ elif abs(m) > self.cross_mult * self.threshold and book.best_ask is not None:
75+ price = int(min(v_hat, book.best_ask))
76+ else:
77+ price = int(min(v_hat, (book.best_bid or mid) + self.improve_ticks))
78+ act.orders.append(OrderIntent(a, Side.BUY, otype, q, price, tag=self.name))
79+ else:
80+ held = obs.positions.get(a, 0)
81+ q = min(q, max(held, 0)) if not obs.short_allowed else q
82+ if q <= 0:
83+ continue
84+ if otype is OrderType.MARKET:
85+ price = None
86+ elif abs(m) > self.cross_mult * self.threshold and book.best_bid is not None:
87+ price = int(max(v_hat, book.best_bid))
88+ else:
89+ price = int(max(v_hat, (book.best_ask or mid) - self.improve_ticks))
90+ act.orders.append(OrderIntent(a, Side.SELL, otype, q, price, tag=self.name))
91+ return act
92+
93+
94+@dataclass
95+class TrendFollower:
96+ """Momentum: signal = (EMA_short - EMA_long)/mid over `mid_history`; buys when positive."""
97+ name: str = "trend"
98+ short_window: int = 5
99+ long_window: int = 30
100+ threshold: float = 0.002
101+ size: int = 20
102+ p_market: float = 0.6
103+ max_qty: int = 50
104+
105+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
106+ act = Action()
107+ for a, book in obs.books.items():
108+ if book.halted:
109+ continue
110+ h = obs.mid_history[a]
111+ if len(h) < self.long_window:
112+ continue
113+ s = h[-self.short_window:].mean() / h[-self.long_window:].mean() - 1.0
114+ if abs(s) < self.threshold:
115+ continue
116+ tc = obs.tick_cents[a]
117+ q = clip_qty(self.size * min(abs(s) / self.threshold, 3.0), min(self.max_qty, obs.max_order_qty))
118+ act.cancels += _cancel_all(obs, a)
119+ otype = _limit_or_market(rng, self.p_market)
120+ if s > 0:
121+ q = min(q, _affordable(obs, book.best_ask or book.mid, tc))
122+ if q <= 0:
123+ continue
124+ price = None if otype is OrderType.MARKET else int(book.best_ask or book.mid)
125+ act.orders.append(OrderIntent(a, Side.BUY, otype, q, price, tag=self.name))
126+ else:
127+ held = obs.positions.get(a, 0)
128+ q = q if obs.short_allowed else min(q, max(held, 0))
129+ if q <= 0:
130+ continue
131+ price = None if otype is OrderType.MARKET else int(book.best_bid or book.mid)
132+ act.orders.append(OrderIntent(a, Side.SELL, otype, q, price, tag=self.name))
133+ return act
134+
135+
136+@dataclass
137+class MeanReverter:
138+ """Contrarian: z = (mid - MA_w)/sd_w; sells when z > thr, buys when z < -thr."""
139+ name: str = "meanrev"
140+ window: int = 40
141+ threshold: float = 1.5
142+ size: int = 15
143+ p_market: float = 0.2
144+ max_qty: int = 50
145+
146+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
147+ act = Action()
148+ for a, book in obs.books.items():
149+ if book.halted:
150+ continue
151+ h = obs.mid_history[a]
152+ if len(h) < self.window:
153+ continue
154+ w = h[-self.window:]
155+ sd = w.std()
156+ if sd <= 0:
157+ continue
158+ z = (book.mid - w.mean()) / sd
159+ if abs(z) < self.threshold:
160+ continue
161+ tc = obs.tick_cents[a]
162+ q = clip_qty(self.size * min(abs(z) / self.threshold, 3.0), min(self.max_qty, obs.max_order_qty))
163+ act.cancels += _cancel_all(obs, a)
164+ otype = _limit_or_market(rng, self.p_market)
165+ if z < 0:
166+ q = min(q, _affordable(obs, book.best_ask or book.mid, tc))
167+ if q <= 0:
168+ continue
169+ price = None if otype is OrderType.MARKET else int((book.best_bid or book.mid) + 1)
170+ act.orders.append(OrderIntent(a, Side.BUY, otype, q, price, tag=self.name))
171+ else:
172+ held = obs.positions.get(a, 0)
173+ q = q if obs.short_allowed else min(q, max(held, 0))
174+ if q <= 0:
175+ continue
176+ price = None if otype is OrderType.MARKET else int((book.best_ask or book.mid) - 1)
177+ act.orders.append(OrderIntent(a, Side.SELL, otype, q, price, tag=self.name))
178+ return act
179+
180+
181+@dataclass
182+class NoiseTrader:
183+ """Zero-intelligence-plus liquidity demander: random side, size ~ 1+Geometric, limit price at
184+ mid ± |N(0, spread_sd)| ticks on the passive side (or a market order with prob p_market)."""
185+ name: str = "noise"
186+ p_market: float = 0.3
187+ mean_size: float = 8.0
188+ price_sd_frac: float = 0.001 # sd of the limit-price offset as a fraction of mid (0.1%)
189+ max_qty: int = 40
190+
191+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
192+ act = Action()
193+ for a, book in obs.books.items():
194+ if book.halted:
195+ continue
196+ side = Side.BUY if rng.random() < 0.5 else Side.SELL
197+ q = clip_qty(1 + rng.geometric(1.0 / self.mean_size), min(self.max_qty, obs.max_order_qty))
198+ tc = obs.tick_cents[a]
199+ if side is Side.BUY:
200+ q = min(q, _affordable(obs, book.best_ask or book.mid, tc))
201+ elif not obs.short_allowed:
202+ q = min(q, max(obs.positions.get(a, 0), 0))
203+ if q <= 0:
204+ continue
205+ if len(obs.open_orders) > 3:
206+ act.cancels += _cancel_all(obs, a)
207+ otype = _limit_or_market(rng, self.p_market)
208+ if otype is OrderType.MARKET:
209+ act.orders.append(OrderIntent(a, side, otype, q, None, tag=self.name))
210+ else:
211+ off = abs(rng.standard_normal()) * self.price_sd_frac * book.mid
212+ ref = (book.best_bid if side is Side.BUY else book.best_ask) or book.mid
213+ price = int(round(ref - off)) if side is Side.BUY else int(round(ref + off))
214+ act.orders.append(OrderIntent(a, side, otype, q, max(price, 1), tag=self.name))
215+ return act
216+
217+
218+@dataclass
219+class MarketMaker:
220+ """Inventory-aware two-sided quoter (bounded Avellaneda–Stoikov-style rule).
221+
222+ Reservation price r = mid * (1 - skew * inv / max_inventory) (inventory skew, fraction of mid)
223+ Half-spread h = clip(base_half + vol_mult * sigma_ticks, min_half, max_half)
224+ with sigma_ticks the std of recent tick-to-tick mid changes. The original A–S formulas assume
225+ Poisson fills and a terminal date; here the skew and spread are bounded fractions of the price so
226+ that quotes always stay inside the exchange price band. Re-quotes every activation."""
227+ name: str = "market_maker"
228+ skew: float = 0.02 # 2% of mid at full inventory deviation
229+ base_half: float = 8.0 # ticks (8 cents on a 200.00 stock = 4 bp half-spread floor)
230+ vol_mult: float = 1.0
231+ quote_qty: int = 20
232+ max_inventory: int = 200 # inventory deviation (lots) at which the skew is maximal
233+ min_half_spread: int = 1
234+ max_half_spread: int = 60
235+ vol_window: int = 30
236+ withdrawn: bool = False # liquidity-shock treatment: stop quoting
237+ _inv0: dict | None = None # inventory target = endowment (set at first decision)
238+
239+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
240+ act = Action()
241+ act.cancels += _cancel_all(obs)
242+ if self.withdrawn:
243+ return act
244+ if self._inv0 is None:
245+ self._inv0 = dict(obs.positions)
246+ for a, book in obs.books.items():
247+ if book.halted:
248+ continue
249+ h = obs.mid_history[a]
250+ sigma = float(np.std(np.diff(h[-self.vol_window:]))) if len(h) > 5 else 2.0
251+ inv = obs.positions.get(a, 0) - self._inv0.get(a, 0)
252+ res = book.mid * (1.0 - self.skew * inv / self.max_inventory)
253+ half = float(np.clip(self.base_half + self.vol_mult * sigma, self.min_half_spread, self.max_half_spread))
254+ bid = int(np.floor(res - half))
255+ ask = int(np.ceil(res + half))
256+ if ask - bid < 2 * self.min_half_spread:
257+ ask = bid + 2 * self.min_half_spread
258+ tc = obs.tick_cents[a]
259+ qb = self.quote_qty if inv < self.max_inventory else 0
260+ qa = self.quote_qty if inv > -self.max_inventory else 0
261+ qb = min(qb, _affordable(obs, bid, tc))
262+ if not obs.short_allowed:
263+ qa = min(qa, max(obs.positions.get(a, 0), 0))
264+ if qb > 0:
265+ act.orders.append(OrderIntent(a, Side.BUY, OrderType.LIMIT, qb, max(bid, 1), tag=self.name))
266+ if qa > 0:
267+ act.orders.append(OrderIntent(a, Side.SELL, OrderType.LIMIT, qa, ask, tag=self.name))
268+ return act
269+
270+
271+@dataclass
272+class StatArb:
273+ """Cross-asset arbitrageur: trades the residual of a rolling regression of asset j's mid on asset
274+ i's mid (pairs trading on the two first assets). Single-asset runs: inactive."""
275+ name: str = "statarb"
276+ window: int = 60
277+ threshold: float = 1.5
278+ size: int = 10
279+ max_qty: int = 40
280+
281+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
282+ act = Action()
283+ assets = sorted(obs.books)
284+ if len(assets) < 2:
285+ return act
286+ i, j = assets[0], assets[1]
287+ hi, hj = obs.mid_history[i], obs.mid_history[j]
288+ n = min(len(hi), len(hj))
289+ if n < self.window:
290+ return act
291+ x, y = np.log(hi[-self.window:]), np.log(hj[-self.window:])
292+ beta = np.cov(x, y)[0, 1] / max(np.var(x), 1e-12)
293+ resid = y - beta * x
294+ z = (resid[-1] - resid.mean()) / max(resid.std(), 1e-9)
295+ if abs(z) < self.threshold:
296+ return act
297+ act.cancels += _cancel_all(obs)
298+ q = clip_qty(self.size, min(self.max_qty, obs.max_order_qty))
299+ bj, bi = obs.books[j], obs.books[i]
300+ if bj.halted or bi.halted:
301+ return act
302+ # z>0: j rich relative to i -> sell j, buy i
303+ sell_a, buy_a = (j, i) if z > 0 else (i, j)
304+ held = obs.positions.get(sell_a, 0)
305+ qs = q if obs.short_allowed else min(q, max(held, 0))
306+ qb = min(q, _affordable(obs, obs.books[buy_a].best_ask or obs.books[buy_a].mid, obs.tick_cents[buy_a]))
307+ if qs > 0:
308+ act.orders.append(OrderIntent(sell_a, Side.SELL, OrderType.LIMIT, qs, int(obs.books[sell_a].best_ask or obs.books[sell_a].mid) - 1, tag=self.name))
309+ if qb > 0:
310+ act.orders.append(OrderIntent(buy_a, Side.BUY, OrderType.LIMIT, qb, int(obs.books[buy_a].best_bid or obs.books[buy_a].mid) + 1, tag=self.name))
311+ return act
312+
313+
314+@dataclass
315+class Leveraged:
316+ """A trend follower that may borrow (leverage_cap > 1 on the account). The policy is identical to
317+ `TrendFollower`; leverage, margin calls and forced liquidation are enforced by the exchange, so
318+ this class exists to make the treatment explicit in configs."""
319+ name: str = "leveraged"
320+ inner: TrendFollower | None = None
321+
322+ def __post_init__(self) -> None:
323+ if self.inner is None:
324+ self.inner = TrendFollower(name="leveraged", size=30, p_market=0.7)
325+
326+ def decide(self, obs: Observation, rng: np.random.Generator) -> Action:
327+ return self.inner.decide(obs, rng) # type: ignore[union-attr]
328+
329+
330+CLASSICAL_POLICIES = {
331+ "fundamentalist": Fundamentalist,
332+ "trend": TrendFollower,
333+ "meanrev": MeanReverter,
334+ "noise": NoiseTrader,
335+ "market_maker": MarketMaker,
336+ "statarb": StatArb,
337+ "leveraged": Leveraged,
338+}
added src/market_agents/analysis/__init__.py +1 −0
@@ -0,0 +1 @@
1+from .estimators import paired_effects, dose_response, ols_seed_fe, holm, local_projection # noqa: F401
added src/market_agents/analysis/estimators.py +126 −0
@@ -0,0 +1,126 @@
1+"""Run-level inference. The unit of observation is one simulation run (cell × seed); trades and ticks
2+within a run are never treated as independent replicates.
3+
4+* paired_effects : matched-seed differences treatment − control with t-based 95 % CI, Cohen's d (paired),
5+ and a Wilcoxon signed-rank p-value.
6+* ols_seed_fe : y = α_seed + Σ_k β_k 1[cell = k] + ε with cluster-robust (by seed) standard errors.
7+* dose_response : effect at each treatment level vs control (paired), with Holm-adjusted p-values.
8+* local_projection: β_h from (gap_{t+h} − gap_{t-1}) regressed on treatment, h = 0..H, around a shock.
9+"""
10+from __future__ import annotations
11+
12+import numpy as np
13+import pandas as pd
14+from scipy import stats
15+
16+
17+def holm(p: np.ndarray) -> np.ndarray:
18+ p = np.asarray(p, dtype=float)
19+ m = np.isfinite(p).sum()
20+ order = np.argsort(np.where(np.isfinite(p), p, np.inf))
21+ adj = np.full_like(p, np.nan)
22+ running = 0.0
23+ for rank, idx in enumerate(order[:m]):
24+ val = min(1.0, (m - rank) * p[idx])
25+ running = max(running, val)
26+ adj[idx] = running
27+ return adj
28+
29+
30+def paired_effects(df: pd.DataFrame, outcome: str, treat_col: str, treat_val, control_val, seed_col: str = "seed") -> dict:
31+ a = df[df[treat_col] == treat_val].set_index(seed_col)[outcome]
32+ b = df[df[treat_col] == control_val].set_index(seed_col)[outcome]
33+ idx = a.index.intersection(b.index)
34+ d = (a.loc[idx] - b.loc[idx]).astype(float).dropna()
35+ n = len(d)
36+ if n < 2:
37+ return {"outcome": outcome, "level": treat_val, "n": n, "diff": float(d.mean()) if n else np.nan}
38+ se = d.std(ddof=1) / np.sqrt(n)
39+ tcrit = stats.t.ppf(0.975, n - 1)
40+ t = d.mean() / se if se > 0 else np.nan
41+ p = 2 * stats.t.sf(abs(t), n - 1) if np.isfinite(t) else np.nan
42+ try:
43+ pw = stats.wilcoxon(d).pvalue if (d != 0).any() else 1.0
44+ except ValueError:
45+ pw = np.nan
46+ return {"outcome": outcome, "level": treat_val, "n": n, "control_mean": float(b.loc[idx].mean()), "treat_mean": float(a.loc[idx].mean()),
47+ "diff": float(d.mean()), "se": float(se), "ci_lo": float(d.mean() - tcrit * se), "ci_hi": float(d.mean() + tcrit * se),
48+ "t": float(t), "p": float(p), "p_wilcoxon": float(pw), "cohen_d": float(d.mean() / d.std(ddof=1)) if d.std(ddof=1) > 0 else np.nan,
49+ "pct_change": float(100 * d.mean() / abs(b.loc[idx].mean())) if b.loc[idx].mean() != 0 else np.nan}
50+
51+
52+def dose_response(df: pd.DataFrame, outcome: str, treat_col: str, control_val, levels: list | None = None, seed_col: str = "seed") -> pd.DataFrame:
53+ levels = levels or sorted(v for v in df[treat_col].unique() if v != control_val)
54+ rows = [paired_effects(df, outcome, treat_col, lv, control_val, seed_col) for lv in levels]
55+ out = pd.DataFrame(rows)
56+ if "p" in out:
57+ out["p_holm"] = holm(out["p"].to_numpy())
58+ return out
59+
60+
61+def ols_seed_fe(df: pd.DataFrame, outcome: str, cell_col: str, control_val, seed_col: str = "seed") -> pd.DataFrame:
62+ """Cluster-robust (by seed) OLS with seed fixed effects; returns one row per non-control cell."""
63+ import statsmodels.formula.api as smf
64+ d = df[[outcome, cell_col, seed_col]].dropna().copy()
65+ d["cell"] = d[cell_col].astype(str)
66+ ctrl = str(control_val)
67+ cats = [ctrl] + sorted(c for c in d["cell"].unique() if c != ctrl)
68+ d["cell"] = pd.Categorical(d["cell"], categories=cats)
69+ d["seed_f"] = d[seed_col].astype(str)
70+ d = d.rename(columns={outcome: "y"})
71+ m = smf.ols("y ~ C(cell) + C(seed_f)", data=d).fit(cov_type="cluster", cov_kwds={"groups": d[seed_col]})
72+ rows = []
73+ for c in cats[1:]:
74+ name = f"C(cell)[T.{c}]"
75+ if name in m.params:
76+ ci = m.conf_int().loc[name]
77+ rows.append({"outcome": outcome, "level": c, "beta": m.params[name], "se": m.bse[name], "ci_lo": ci[0], "ci_hi": ci[1],
78+ "p": m.pvalues[name], "n": int(m.nobs), "r2": m.rsquared})
79+ out = pd.DataFrame(rows)
80+ if len(out):
81+ out["p_holm"] = holm(out["p"].to_numpy())
82+ return out
83+
84+
85+def local_projection(ticks_by_run: dict, treat: dict, shock_t: int, H: int = 40, control_val=0.0) -> pd.DataFrame:
86+ """ticks_by_run: {run_id: ticks DataFrame}; treat: {run_id: treatment value}. Gap = log(mid) - log(full_value).
87+ For each horizon h, regress (gap_{t+h} - gap_{t-1}) on 1[treated] across runs (paired by seed not required)."""
88+ rows = []
89+ gaps = {}
90+ for rid, tk in ticks_by_run.items():
91+ d = tk[tk.asset == 0].set_index("t")
92+ g = np.log(d["mid"]) - np.log(d["full_value"])
93+ gaps[rid] = g
94+ for h in range(0, H + 1):
95+ y, x = [], []
96+ for rid, g in gaps.items():
97+ if shock_t - 1 in g.index and shock_t + h in g.index:
98+ y.append(g.loc[shock_t + h] - g.loc[shock_t - 1])
99+ x.append(1.0 if treat[rid] != control_val else 0.0)
100+ y, x = np.array(y), np.array(x)
101+ if len(y) < 4 or x.var() == 0:
102+ continue
103+ X = np.column_stack([np.ones_like(x), x])
104+ beta, *_ = np.linalg.lstsq(X, y, rcond=None)
105+ resid = y - X @ beta
106+ # HC1 robust SE
107+ XtX_inv = np.linalg.inv(X.T @ X)
108+ V = XtX_inv @ (X.T * resid ** 2) @ X @ XtX_inv * len(y) / (len(y) - 2)
109+ rows.append({"h": h, "beta": beta[1], "se": np.sqrt(V[1, 1]), "control_path": float(y[x == 0].mean()), "treated_path": float(y[x == 1].mean())})
110+ return pd.DataFrame(rows)
111+
112+
113+def power_seeds(d: float, power: float = 0.8, alpha: float = 0.05) -> int:
114+ """Paired design: seeds needed to detect a standardized effect d (one-sample t approximation)."""
115+ z = stats.norm.ppf(1 - alpha / 2) + stats.norm.ppf(power)
116+ return int(np.ceil((z / d) ** 2))
117+
118+
119+def summarize_cells(df: pd.DataFrame, outcomes: list[str], cell_col: str = "name") -> pd.DataFrame:
120+ g = df.groupby(cell_col)[outcomes]
121+ m, s, n = g.mean(), g.std(ddof=1), g.size()
122+ out = m.copy()
123+ for c in outcomes:
124+ out[c + "_se"] = s[c] / np.sqrt(n)
125+ out["n_runs"] = n
126+ return out
added src/market_agents/analysis/plots.py +137 −0
@@ -0,0 +1,137 @@
1+"""Publication figures (matplotlib, vector PDF). Palette validated with the dataviz checker (light surface)."""
2+from __future__ import annotations
3+
4+from pathlib import Path
5+
6+import matplotlib
7+matplotlib.use("Agg")
8+import matplotlib.pyplot as plt # noqa: E402
9+import numpy as np # noqa: E402
10+import pandas as pd # noqa: E402
11+
12+SERIES = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#6250d6", "#008300", "#e34948"]
13+INK, INK2, GRID = "#0b0b0b", "#52514e", "#e6e5e1"
14+
15+plt.rcParams.update({
16+ "font.family": "serif", "font.size": 9, "axes.labelsize": 9, "axes.titlesize": 9.5, "legend.fontsize": 8,
17+ "xtick.labelsize": 8, "ytick.labelsize": 8, "axes.edgecolor": INK2, "axes.linewidth": 0.6, "axes.grid": True,
18+ "grid.color": GRID, "grid.linewidth": 0.5, "axes.spines.top": False, "axes.spines.right": False,
19+ "lines.linewidth": 1.6, "figure.dpi": 150, "savefig.bbox": "tight", "pdf.fonttype": 42, "text.color": INK,
20+ "axes.labelcolor": INK, "xtick.color": INK2, "ytick.color": INK2, "legend.frameon": False,
21+})
22+
23+
24+def save(fig, path: str | Path) -> None:
25+ path = Path(path)
26+ path.parent.mkdir(parents=True, exist_ok=True)
27+ fig.savefig(path)
28+ fig.savefig(path.with_suffix(".png"), dpi=200)
29+ plt.close(fig)
30+
31+
32+def price_paths(runs: list[tuple[str, pd.DataFrame]], shock_t: int | None = None, title: str = "") -> plt.Figure:
33+ """Mid vs latent value for several runs (one panel per run)."""
34+ n = len(runs)
35+ fig, axes = plt.subplots(1, n, figsize=(2.9 * n, 2.4), sharey=True)
36+ axes = np.atleast_1d(axes)
37+ for ax, (label, tk), c in zip(axes, runs, SERIES):
38+ d = tk[tk.asset == 0]
39+ ax.plot(d.t, d.full_value / 100, color=INK2, lw=1.0, ls="--", label="latent value")
40+ ax.plot(d.t, d.public_value / 100, color=INK2, lw=0.7, ls=":", label="public value")
41+ ax.plot(d.t, d.mid / 100, color=c, lw=1.2, label="mid")
42+ if shock_t:
43+ ax.axvline(shock_t, color=GRID, lw=1.0)
44+ ax.set_title(label)
45+ ax.set_xlabel("tick")
46+ axes[0].set_ylabel("price")
47+ axes[0].legend(loc="upper left")
48+ if title:
49+ fig.suptitle(title, fontsize=9.5)
50+ return fig
51+
52+
53+def dose_response(eff: dict[str, pd.DataFrame], ylabels: dict[str, str], x_col: str = "level", ncols: int = 3) -> plt.Figure:
54+ """eff: outcome -> DataFrame with columns level, diff, ci_lo, ci_hi (paired effects vs control)."""
55+ keys = list(eff)
56+ nrows = int(np.ceil(len(keys) / ncols))
57+ fig, axes = plt.subplots(nrows, ncols, figsize=(2.6 * ncols, 2.1 * nrows))
58+ axes = np.atleast_1d(axes).ravel()
59+ for ax, k in zip(axes, keys):
60+ d = eff[k]
61+ x = d[x_col].astype(float).to_numpy()
62+ ax.axhline(0, color=INK2, lw=0.7)
63+ ax.fill_between(x, d.ci_lo, d.ci_hi, color=SERIES[0], alpha=0.18, lw=0)
64+ ax.plot(x, d["diff"], color=SERIES[0], marker="o", ms=4)
65+ ax.set_title(ylabels.get(k, k))
66+ ax.set_xlabel("LLM share of trader slots")
67+ for ax in axes[len(keys):]:
68+ ax.axis("off")
69+ return fig
70+
71+
72+def cell_bars(df: pd.DataFrame, outcome: str, cells: list[str], labels: dict[str, str] | None = None, ylabel: str = "",
73+ control: str | None = None) -> plt.Figure:
74+ g = df[df.cell.isin(cells)].groupby("cell")[outcome]
75+ m, se = g.mean().reindex(cells), (g.std(ddof=1) / np.sqrt(g.size())).reindex(cells)
76+ fig, ax = plt.subplots(figsize=(0.55 * len(cells) + 1.5, 2.3))
77+ cols = [SERIES[0] if c != control else INK2 for c in cells]
78+ ax.bar(range(len(cells)), m, yerr=1.96 * se, color=cols, width=0.62, error_kw={"lw": 0.8, "capsize": 2})
79+ ax.set_xticks(range(len(cells)))
80+ ax.set_xticklabels([(labels or {}).get(c, c) for c in cells], rotation=30, ha="right")
81+ ax.set_ylabel(ylabel or outcome)
82+ ax.grid(axis="x", visible=False)
83+ return fig
84+
85+
86+def local_projection_plot(lp: dict[str, pd.DataFrame], title: str = "") -> plt.Figure:
87+ fig, ax = plt.subplots(figsize=(3.6, 2.4))
88+ for (label, d), c in zip(lp.items(), SERIES):
89+ ax.plot(d.h, 100 * d.beta, color=c, label=label)
90+ ax.fill_between(d.h, 100 * (d.beta - 1.96 * d.se), 100 * (d.beta + 1.96 * d.se), color=c, alpha=0.15, lw=0)
91+ ax.axhline(0, color=INK2, lw=0.7)
92+ ax.set_xlabel("ticks after the fundamental shock")
93+ ax.set_ylabel("treatment effect on log(P/V) gap, %")
94+ ax.legend()
95+ if title:
96+ ax.set_title(title)
97+ return fig
98+
99+
100+def scatter_mechanism(df: pd.DataFrame, x: str, y: str, hue: str, xlabel: str, ylabel: str) -> plt.Figure:
101+ fig, ax = plt.subplots(figsize=(3.4, 2.6))
102+ for (k, d), c in zip(df.groupby(hue), SERIES):
103+ ax.scatter(d[x], d[y], s=14, color=c, label=str(k), alpha=0.85, edgecolor="white", linewidth=0.4)
104+ ax.set_xlabel(xlabel)
105+ ax.set_ylabel(ylabel)
106+ ax.legend(title=hue)
107+ return fig
108+
109+
110+def wealth_and_inventory(agents: pd.DataFrame, ticks: pd.DataFrame | None = None) -> plt.Figure:
111+ fig, axes = plt.subplots(1, 2, figsize=(6.0, 2.4))
112+ ax = axes[0]
113+ for (pol, d), c in zip(agents.groupby("policy"), SERIES):
114+ ax.hist(d["pnl_pct"], bins=25, alpha=0.6, color=c, label=pol)
115+ ax.set_xlabel("P&L, % of initial wealth")
116+ ax.set_ylabel("traders")
117+ ax.legend()
118+ ax = axes[1]
119+ if ticks is not None:
120+ d = ticks[ticks.asset == 0]
121+ ax.plot(d.t, d.depth_bid5, color=SERIES[0], label="bid depth (5 levels)")
122+ ax.plot(d.t, d.depth_ask5, color=SERIES[1], label="ask depth")
123+ ax.set_xlabel("tick")
124+ ax.set_ylabel("lots")
125+ ax.legend()
126+ return fig
127+
128+
129+def cost_quality(df: pd.DataFrame, xcol: str, ycol: str, label_col: str, xlabel: str, ylabel: str) -> plt.Figure:
130+ fig, ax = plt.subplots(figsize=(3.4, 2.6))
131+ for (k, d), c in zip(df.groupby(label_col), SERIES):
132+ ax.errorbar(d[xcol].mean(), d[ycol].mean(), yerr=1.96 * d[ycol].std(ddof=1) / np.sqrt(len(d)) if len(d) > 1 else 0,
133+ fmt="o", color=c, ms=5, capsize=2, label=str(k))
134+ ax.set_xlabel(xlabel)
135+ ax.set_ylabel(ylabel)
136+ ax.legend()
137+ return fig
added src/market_agents/assets/__init__.py +0 −0
added src/market_agents/assets/fundamentals.py +165 −0
@@ -0,0 +1,165 @@
1+"""Exogenous fundamental processes and the present-value benchmarks.
2+
3+Per asset k, the dividend paid every `div_interval` ticks is
4+ D_t = mu_{s_t} + x_t + J_t, x_t = rho x_{t-1} + sigma eps_t,
5+with a Markov regime s_t in {0,1} shifting the mean dividend mu, and a jump J_t (compound Poisson,
6+only at dividend dates). Innovations eps are correlated across assets through a one-factor structure
7+ eps_k = b_k f + sqrt(1 - b_k^2) u_k.
8+
9+Benchmarks (cents per lot, tick-time discount rate r applied per dividend period):
10+- `full_value` : E[PV | true state (mu_{s_t}, x_t)] -> the "latent fundamental value"
11+- `public_value`: E[PV | public information only] -> Kalman/HMM posterior on (s_t, x_t)
12+Both use the same closed form V = mu/r_d + x * rho_d/(1 + r_d - rho_d) where r_d, rho_d are the
13+per-dividend-period rate and persistence; agents never see `full_value`.
14+"""
15+from __future__ import annotations
16+
17+from dataclasses import dataclass, field
18+
19+import numpy as np
20+
21+
22+@dataclass(slots=True)
23+class FundamentalConfig:
24+ n_assets: int = 1
25+ div_interval: int = 20 # ticks between dividends
26+ r_period: float = 0.0025 # discount rate per dividend period (0.25%) -> P/D ~ 400
27+ mu: tuple[float, ...] = (50.0,) # mean dividend per period in cents, regime 0 (per asset)
28+ mu_shift: float = 0.0 # regime 1 mean = mu * (1 + mu_shift); 0 => no regimes
29+ p_switch: float = 0.0 # per-period probability of a regime switch
30+ rho: float = 0.98 # AR(1) persistence of x per period
31+ sigma: float = 2.0 # innovation std of x (cents)
32+ jump_prob: float = 0.0 # per-period jump probability
33+ jump_sigma: float = 0.0 # jump std (cents of dividend)
34+ factor_loading: tuple[float, ...] = (0.0,) # b_k per asset (common factor in innovations)
35+ x0: float = 0.0
36+
37+
38+@dataclass
39+class FundamentalState:
40+ regime: np.ndarray # (K,) int
41+ x: np.ndarray # (K,) float, latent deviation
42+ period: int = 0
43+ # public-information posterior (per asset): mean and variance of x, regime probability
44+ x_hat: np.ndarray = field(default_factory=lambda: np.zeros(1))
45+ x_var: np.ndarray = field(default_factory=lambda: np.zeros(1))
46+ p_regime1: np.ndarray = field(default_factory=lambda: np.zeros(1))
47+ x_hat_revealed: np.ndarray = field(default_factory=lambda: np.zeros(1))
48+ x_var_revealed: np.ndarray = field(default_factory=lambda: np.zeros(1))
49+
50+
51+class Fundamentals:
52+ """Draws the dividend path with a dedicated RNG stream so shocks are common random numbers."""
53+
54+ def __init__(self, cfg: FundamentalConfig, rng: np.random.Generator) -> None:
55+ self.cfg = cfg
56+ self.rng = rng
57+ K = cfg.n_assets
58+ mu = np.asarray(cfg.mu, dtype=float)
59+ self.mu = np.resize(mu, K)
60+ b = np.asarray(cfg.factor_loading, dtype=float)
61+ self.b = np.resize(b, K)
62+ self.state = FundamentalState(regime=np.zeros(K, dtype=int), x=np.full(K, cfg.x0, dtype=float),
63+ x_hat=np.full(K, cfg.x0, dtype=float),
64+ x_var=np.zeros(K), p_regime1=np.zeros(K),
65+ x_hat_revealed=np.full(K, cfg.x0, dtype=float), x_var_revealed=np.zeros(K))
66+ self.history: list[dict] = []
67+ self.x_revealed = np.full(K, cfg.x0, dtype=float) # last announced deviation
68+ self.last_dividend = self.regime_mean(self.state.regime) + self.x_revealed
69+ self._draw_next() # x_1 is latent from t = 0
70+
71+ def _draw_next(self) -> None:
72+ """Draw the upcoming (latent) deviation from the last revealed one and reset the public prior."""
73+ cfg, st, K = self.cfg, self.state, self.cfg.n_assets
74+ f = self.rng.standard_normal()
75+ u = self.rng.standard_normal(K)
76+ eps = self.b * f + np.sqrt(np.clip(1.0 - self.b ** 2, 0.0, 1.0)) * u
77+ st.x = cfg.rho * self.x_revealed + cfg.sigma * eps
78+ st.x_hat = cfg.rho * st.x_hat_revealed if hasattr(st, "x_hat_revealed") else cfg.rho * self.x_revealed
79+ st.x_var = cfg.rho ** 2 * getattr(st, "x_var_revealed", np.zeros(K)) + cfg.sigma ** 2
80+
81+ # ---- closed-form PV ------------------------------------------------------------------
82+ def _pv(self, mu: np.ndarray, x: np.ndarray) -> np.ndarray:
83+ r, rho = self.cfg.r_period, self.cfg.rho
84+ return mu / r + x / (1.0 + r - rho)
85+
86+ def regime_mean(self, regime: np.ndarray) -> np.ndarray:
87+ return self.mu * (1.0 + self.cfg.mu_shift * regime)
88+
89+ def full_value(self) -> np.ndarray:
90+ """Latent fundamental value in cents per lot (never shown to agents)."""
91+ return self._pv(self.regime_mean(self.state.regime), self.state.x)
92+
93+ def public_value(self) -> np.ndarray:
94+ """Expected PV under the public information set (filtered state)."""
95+ p1 = self.state.p_regime1
96+ mu = self.mu * (1.0 + self.cfg.mu_shift * p1)
97+ return self._pv(mu, self.state.x_hat)
98+
99+ def dividend_now(self) -> np.ndarray:
100+ return self.regime_mean(self.state.regime) + self.state.x
101+
102+ # ---- dynamics --------------------------------------------------------------------------
103+ def step(self) -> np.ndarray:
104+ """Pay the dividend of the current period (revealing x_n), then draw x_{n+1}. Returns D_n (cents)."""
105+ cfg, st, K = self.cfg, self.state, self.cfg.n_assets
106+ if cfg.p_switch > 0:
107+ flips = self.rng.random(K) < cfg.p_switch
108+ st.regime = np.where(flips, 1 - st.regime, st.regime)
109+ jump = np.zeros(K)
110+ if cfg.jump_prob > 0:
111+ hit = self.rng.random(K) < cfg.jump_prob
112+ jump = np.where(hit, self.rng.standard_normal(K) * cfg.jump_sigma, 0.0)
113+ st.period += 1
114+ d = self.regime_mean(st.regime) + st.x + jump
115+ self.last_dividend = d
116+ self.x_revealed = st.x.copy()
117+ self._update_public(d) # sets x_hat_revealed / x_var_revealed (exact when jumps and regimes are off)
118+ self._draw_next()
119+ self.history.append({"period": st.period, "dividend": d.copy(), "x": st.x.copy(), "x_revealed": self.x_revealed.copy(),
120+ "regime": st.regime.copy(), "full_value": self.full_value(), "public_value": self.public_value()})
121+ return d
122+
123+ def _update_public(self, d: np.ndarray) -> None:
124+ """Public inference from the announced dividend: with one regime and no jumps the deviation is read
125+ exactly (x_revealed = d - mu); with regimes it is a two-state mixture filter; with jumps a Kalman step."""
126+ cfg, st = self.cfg, self.state
127+ obs_var = cfg.jump_prob * cfg.jump_sigma ** 2 + 1e-9
128+ x_pred, v_pred = st.x_hat, st.x_var # prior on x_n carried through the period (news-updated)
129+ if cfg.mu_shift == 0.0 or cfg.p_switch == 0.0:
130+ K_gain = v_pred / (v_pred + obs_var)
131+ st.x_hat_revealed = x_pred + K_gain * (d - self.mu - x_pred)
132+ st.x_var_revealed = (1 - K_gain) * v_pred
133+ st.p_regime1 = np.zeros_like(st.p_regime1)
134+ return
135+ p1 = st.p_regime1 * (1 - cfg.p_switch) + (1 - st.p_regime1) * cfg.p_switch
136+ mu0, mu1 = self.mu, self.mu * (1 + cfg.mu_shift)
137+ s2 = v_pred + obs_var
138+ l0 = np.exp(-0.5 * (d - mu0 - x_pred) ** 2 / s2)
139+ l1 = np.exp(-0.5 * (d - mu1 - x_pred) ** 2 / s2)
140+ post1 = p1 * l1 / (p1 * l1 + (1 - p1) * l0 + 1e-300)
141+ K_gain = v_pred / s2
142+ innov = d - (post1 * mu1 + (1 - post1) * mu0) - x_pred
143+ st.x_hat_revealed = x_pred + K_gain * innov
144+ st.x_var_revealed = (1 - K_gain) * v_pred
145+ st.p_regime1 = post1
146+
147+
148+def public_observe(f: "Fundamentals", asset: int, value: float, noise_sd: float) -> None:
149+ """Kalman update of the public posterior on x_t from a public news item reporting x_t with stated noise.
150+ Misleading items carry an undisclosed bias, so the public filter is (deliberately) fooled by them."""
151+ st = f.state
152+ v = st.x_var[asset]
153+ R = max(noise_sd, 0.1) ** 2
154+ K = v / (v + R)
155+ st.x_hat[asset] = st.x_hat[asset] + K * (value - st.x_hat[asset])
156+ st.x_var[asset] = (1.0 - K) * v
157+
158+
159+def private_signal(f: Fundamentals, rng: np.random.Generator, precision: float, asset: int) -> float:
160+ """A noisy private signal about the latent deviation of the *upcoming* dividend: s = x_n + N(0, sigma^2/precision).
161+
162+ precision -> inf : perfect knowledge of x_n ; precision -> 0 : pure noise. x_n is already drawn (it is
163+ the state of the economy), so this is information about the latent state, not about future random draws."""
164+ noise_sd = f.cfg.sigma / np.sqrt(max(precision, 1e-9))
165+ return float(f.state.x[asset] + rng.standard_normal() * noise_sd)
added src/market_agents/cli.py +68 −0
@@ -0,0 +1,68 @@
1+"""`ma` command line: run one config, run a design, analyze, estimate budget."""
2+from __future__ import annotations
3+
4+import argparse
5+import json
6+import sys
7+from pathlib import Path
8+
9+
10+def main(argv=None) -> None:
11+ ap = argparse.ArgumentParser(prog="ma")
12+ sub = ap.add_subparsers(dest="cmd", required=True)
13+ r = sub.add_parser("run", help="run one configuration")
14+ r.add_argument("config"); r.add_argument("--seed", type=int); r.add_argument("--out"); r.add_argument("--set", nargs="*", default=[])
15+ r.add_argument("--cache"); r.add_argument("--progress", action="store_true")
16+ d = sub.add_parser("design", help="run a design file (cells x seeds)")
17+ d.add_argument("design"); d.add_argument("--workers", type=int, default=4); d.add_argument("--cache"); d.add_argument("--cells", nargs="*")
18+ d.add_argument("--max-seeds", type=int); d.add_argument("--root", default="results/runs")
19+ b = sub.add_parser("budget", help="estimate API calls and USD for a design")
20+ b.add_argument("design"); b.add_argument("--tokens-in", type=int, default=800); b.add_argument("--tokens-out", type=int, default=250)
21+ args = ap.parse_args(argv)
22+ from dotenv import load_dotenv
23+ load_dotenv(Path(".env"), override=False)
24+ if args.cmd == "run":
25+ from .experiments.config import RunConfig
26+ from .experiments.runner import run_config
27+ from .metrics.outcomes import run_outcomes
28+ ov = {}
29+ for kv in args.set:
30+ k, v = kv.split("=", 1)
31+ ov[k] = json.loads(v) if v[:1] in "[{\"0123456789-tfn" and v not in ("true", "false") else v
32+ if v in ("true", "false"):
33+ ov[k] = v == "true"
34+ cfg = RunConfig.from_yaml(args.config, **ov)
35+ if args.seed is not None:
36+ cfg = cfg.with_overrides(seed=args.seed)
37+ res = run_config(cfg, out_dir=args.out, cache_path=args.cache, progress=args.progress)
38+ oc = run_outcomes(res)
39+ print(json.dumps({k: (round(v, 4) if isinstance(v, float) else v) for k, v in oc.items()}, indent=1, default=str))
40+ elif args.cmd == "design":
41+ from .experiments.design import run_design
42+ df = run_design(args.design, args.root, args.workers, args.cache, args.cells, args.max_seeds)
43+ print(df.groupby("name")[[c for c in ("msd_log", "spread_bp", "n_bubbles", "llm_usd") if c in df]].mean().round(4))
44+ elif args.cmd == "budget":
45+ from .experiments.design import cell_config, expand_cells, load_design, seeds_of
46+ design = load_design(args.design)
47+ total_calls, total_usd = 0, 0.0
48+ for c in expand_cells(design):
49+ for s in seeds_of(design):
50+ cfg = cell_config(design, c, s, Path(args.design).parent)
51+ p = cfg.population
52+ n_llm = int(round(p.llm_share * p.n_traders))
53+ n_dec = (cfg.n_ticks - cfg.warmup // 2) // p.llm.decision_interval
54+ if p.llm.decision_cap:
55+ n_dec = min(n_dec, p.llm.decision_cap)
56+ calls = n_llm * n_dec
57+ usd = 0.0
58+ for prov, share in p.llm.provider_mix.items():
59+ model = p.llm.models.get(prov, "")
60+ pr = p.llm.price_assumptions.get(model, {"input": 0, "output": 0})
61+ usd += calls * share * (args.tokens_in * pr["input"] + args.tokens_out * pr["output"]) / 1e6
62+ total_calls += calls
63+ total_usd += usd
64+ print(f"design {design['name']}: {total_calls} LLM calls, est. ${total_usd:.2f} at {args.tokens_in} in / {args.tokens_out} out tokens per call")
65+
66+
67+if __name__ == "__main__":
68+ main(sys.argv[1:])
added src/market_agents/exchange/__init__.py +0 −0
added src/market_agents/exchange/accounts.py +120 −0
@@ -0,0 +1,120 @@
1+"""Balance sheets with exact integer accounting and explicit reservations.
2+
3+Invariant enforced by `Ledger.check_conservation()` after every fill:
4+ sum(cash of all agents) + exchange_fees_collected == initial_cash_total + dividends_paid_total
5+ sum(positions of all agents per asset) == initial_shares_total per asset (the exchange never creates shares)
6+"""
7+from __future__ import annotations
8+
9+from dataclasses import dataclass, field
10+
11+
12+@dataclass(slots=True)
13+class Account:
14+ agent_id: int
15+ cash: int # cents
16+ positions: dict[int, int] # asset -> lots (negative if short)
17+ reserved_cash: int = 0 # cents locked by open bids
18+ reserved_qty: dict[int, int] = field(default_factory=dict) # asset -> lots locked by open asks
19+ frozen: bool = False # bankrupt / liquidated: no new orders
20+ realized_fees: int = 0 # cents paid in fees (cumulative)
21+ dividends_received: int = 0
22+ n_orders: int = 0
23+ n_fills: int = 0
24+ n_rejects: int = 0
25+ n_cancels: int = 0
26+ traded_qty: int = 0
27+ # risk settings (per agent)
28+ leverage_cap: float = 1.0 # max gross exposure / equity (1.0 = no borrowing)
29+ short_allowed: bool = False
30+ maintenance_margin: float = 0.25 # equity / gross exposure must stay above this if leveraged
31+ initial_cash: int = 0
32+ initial_positions: dict[int, int] = field(default_factory=dict)
33+
34+ @property
35+ def free_cash(self) -> int:
36+ return self.cash - self.reserved_cash
37+
38+ def free_qty(self, asset: int) -> int:
39+ return self.positions.get(asset, 0) - self.reserved_qty.get(asset, 0)
40+
41+ def equity(self, marks: dict[int, float]) -> float:
42+ """Cash + marked positions (marks in cents per lot)."""
43+ return self.cash + sum(q * marks.get(a, 0.0) for a, q in self.positions.items())
44+
45+ def gross_exposure(self, marks: dict[int, float]) -> float:
46+ return sum(abs(q) * marks.get(a, 0.0) for a, q in self.positions.items())
47+
48+ def net_worth_units(self, marks: dict[int, float]) -> float:
49+ return self.equity(marks) / 100.0
50+
51+
52+class Ledger:
53+ """All accounts plus the exchange's own fee account; owns the conservation checks."""
54+
55+ def __init__(self) -> None:
56+ self.accounts: dict[int, Account] = {}
57+ self.exchange_fees: int = 0
58+ self.dividends_paid: int = 0
59+ self.initial_cash_total: int = 0
60+ self.initial_shares: dict[int, int] = {}
61+
62+ def add(self, acct: Account) -> None:
63+ acct.initial_cash = acct.cash
64+ acct.initial_positions = dict(acct.positions)
65+ self.accounts[acct.agent_id] = acct
66+ self.initial_cash_total += acct.cash
67+ for a, q in acct.positions.items():
68+ self.initial_shares[a] = self.initial_shares.get(a, 0) + q
69+
70+ def __getitem__(self, agent_id: int) -> Account:
71+ return self.accounts[agent_id]
72+
73+ def apply_fill(self, buyer: int, seller: int, asset: int, price_cents: int, qty: int,
74+ buyer_fee: int, seller_fee: int, buyer_reserved: int, seller_reserved_qty: int) -> None:
75+ """Move cash and lots; release the reservations attached to the two orders for this quantity."""
76+ notional = price_cents * qty
77+ b, s = self.accounts[buyer], self.accounts[seller]
78+ b.cash -= notional + buyer_fee
79+ s.cash += notional - seller_fee
80+ b.positions[asset] = b.positions.get(asset, 0) + qty
81+ s.positions[asset] = s.positions.get(asset, 0) - qty
82+ b.reserved_cash -= buyer_reserved
83+ s.reserved_qty[asset] = s.reserved_qty.get(asset, 0) - seller_reserved_qty
84+ b.realized_fees += buyer_fee
85+ s.realized_fees += seller_fee
86+ b.n_fills += 1
87+ s.n_fills += 1
88+ b.traded_qty += qty
89+ s.traded_qty += qty
90+ self.exchange_fees += buyer_fee + seller_fee
91+ assert b.reserved_cash >= 0, "negative cash reservation"
92+ assert s.reserved_qty[asset] >= 0 or s.short_allowed, "negative qty reservation"
93+
94+ def pay_dividend(self, asset: int, per_lot_cents: int) -> int:
95+ """Pays (or charges, for shorts) a cash dividend; returns the net cash injected."""
96+ total = 0
97+ for acct in self.accounts.values():
98+ q = acct.positions.get(asset, 0)
99+ if q:
100+ d = per_lot_cents * q
101+ acct.cash += d
102+ acct.dividends_received += d
103+ total += d
104+ self.dividends_paid += total
105+ return total
106+
107+ def check_conservation(self) -> None:
108+ cash = sum(a.cash for a in self.accounts.values()) + self.exchange_fees
109+ assert cash == self.initial_cash_total + self.dividends_paid, (
110+ f"cash not conserved: {cash} != {self.initial_cash_total} + {self.dividends_paid}")
111+ for asset, n0 in self.initial_shares.items():
112+ n = sum(a.positions.get(asset, 0) for a in self.accounts.values())
113+ assert n == n0, f"shares not conserved for asset {asset}: {n} != {n0}"
114+ for a in self.accounts.values():
115+ assert a.reserved_cash >= 0 and a.reserved_cash <= max(a.cash, 0) or a.leverage_cap > 1.0, (
116+ f"agent {a.agent_id}: reserved_cash {a.reserved_cash} > cash {a.cash}")
117+ for asset, rq in a.reserved_qty.items():
118+ assert rq >= 0, f"agent {a.agent_id}: negative reserved qty"
119+ assert a.short_allowed or rq <= a.positions.get(asset, 0), (
120+ f"agent {a.agent_id}: reserved {rq} > held {a.positions.get(asset, 0)} on asset {asset}")
added src/market_agents/exchange/book.py +190 −0
@@ -0,0 +1,190 @@
1+"""Single-asset limit order book with price-time priority.
2+
3+The book only knows orders and prices. Fees, accounts, latency and halts live in `engine.py`.
4+"""
5+from __future__ import annotations
6+
7+from collections import deque
8+from dataclasses import dataclass
9+from typing import Iterator
10+
11+from sortedcontainers import SortedDict
12+
13+from .orders import Order, OrderStatus, OrderType, Side
14+
15+
16+@dataclass(slots=True)
17+class Fill:
18+ resting: Order
19+ incoming: Order
20+ price: int
21+ qty: int
22+
23+
24+class LimitOrderBook:
25+ __slots__ = ("asset", "bids", "asks", "_index", "_depth_cache")
26+
27+ def __init__(self, asset: int) -> None:
28+ self.asset = asset
29+ # bids: highest price first -> store negative price keys so .peekitem(0) is the best bid
30+ self.bids: SortedDict[int, deque[Order]] = SortedDict()
31+ self.asks: SortedDict[int, deque[Order]] = SortedDict()
32+ self._index: dict[int, Order] = {}
33+
34+ # ---- queries -------------------------------------------------------------------------
35+ def best_bid(self) -> int | None:
36+ return -self.bids.peekitem(0)[0] if self.bids else None
37+
38+ def best_ask(self) -> int | None:
39+ return self.asks.peekitem(0)[0] if self.asks else None
40+
41+ def mid(self) -> float | None:
42+ b, a = self.best_bid(), self.best_ask()
43+ if b is None or a is None:
44+ return None
45+ return (a + b) / 2.0
46+
47+ def spread(self) -> int | None:
48+ b, a = self.best_bid(), self.best_ask()
49+ if b is None or a is None:
50+ return None
51+ return a - b
52+
53+ def depth(self, levels: int = 5) -> tuple[list[tuple[int, int]], list[tuple[int, int]]]:
54+ """Aggregated (price, qty) for the best `levels` on each side."""
55+ bids = []
56+ for i, (negp, q) in enumerate(self.bids.items()):
57+ if i >= levels:
58+ break
59+ bids.append((-negp, sum(o.remaining for o in q)))
60+ asks = []
61+ for i, (p, q) in enumerate(self.asks.items()):
62+ if i >= levels:
63+ break
64+ asks.append((p, sum(o.remaining for o in q)))
65+ return bids, asks
66+
67+ def depth_within(self, side: Side, ticks: int) -> int:
68+ """Total resting quantity within `ticks` of the best price on `side`."""
69+ if side is Side.BUY:
70+ bb = self.best_bid()
71+ if bb is None:
72+ return 0
73+ return sum(o.remaining for negp, q in self.bids.items() if -negp >= bb - ticks for o in q)
74+ ba = self.best_ask()
75+ if ba is None:
76+ return 0
77+ return sum(o.remaining for p, q in self.asks.items() if p <= ba + ticks for o in q)
78+
79+ def get(self, order_id: int) -> Order | None:
80+ return self._index.get(order_id)
81+
82+ def __contains__(self, order_id: int) -> bool:
83+ return order_id in self._index
84+
85+ def __len__(self) -> int:
86+ return len(self._index)
87+
88+ def resting_orders(self) -> Iterator[Order]:
89+ return iter(self._index.values())
90+
91+ def resting_for(self, agent_id: int) -> list[Order]:
92+ return [o for o in self._index.values() if o.agent_id == agent_id]
93+
94+ def total_qty(self) -> tuple[int, int]:
95+ return (sum(o.remaining for q in self.bids.values() for o in q),
96+ sum(o.remaining for q in self.asks.values() for o in q))
97+
98+ # ---- mutation ------------------------------------------------------------------------
99+ def _rest(self, order: Order) -> None:
100+ assert order.order_type is OrderType.LIMIT and order.price is not None
101+ book = self.bids if order.side is Side.BUY else self.asks
102+ key = -order.price if order.side is Side.BUY else order.price
103+ q = book.get(key)
104+ if q is None:
105+ q = deque()
106+ book[key] = q
107+ q.append(order)
108+ self._index[order.order_id] = order
109+ order.status = OrderStatus.OPEN
110+
111+ def _remove_resting(self, order: Order) -> None:
112+ book = self.bids if order.side is Side.BUY else self.asks
113+ key = -order.price if order.side is Side.BUY else order.price
114+ q = book[key]
115+ q.remove(order)
116+ if not q:
117+ del book[key]
118+ del self._index[order.order_id]
119+
120+ def match(self, incoming: Order, max_qty: int | None = None) -> list[Fill]:
121+ """Walk the opposite side while the incoming order crosses.
122+
123+ Returns the fills in execution order; mutates `incoming.remaining` and the resting orders.
124+ Does *not* rest the remainder (the engine decides, because it must reserve cash first).
125+ `max_qty` caps the executable quantity (used for market buys with a cash constraint).
126+ """
127+ fills: list[Fill] = []
128+ opp = self.asks if incoming.side is Side.BUY else self.bids
129+ budget = incoming.remaining if max_qty is None else min(incoming.remaining, max_qty)
130+ while budget > 0 and opp:
131+ key, queue = opp.peekitem(0)
132+ best_price = key if incoming.side is Side.BUY else -key
133+ if incoming.order_type is OrderType.LIMIT:
134+ assert incoming.price is not None
135+ if incoming.side is Side.BUY and best_price > incoming.price:
136+ break
137+ if incoming.side is Side.SELL and best_price < incoming.price:
138+ break
139+ while budget > 0 and queue:
140+ resting = queue[0]
141+ q = min(resting.remaining, budget)
142+ resting.remaining -= q
143+ incoming.remaining -= q
144+ budget -= q
145+ fills.append(Fill(resting=resting, incoming=incoming, price=best_price, qty=q))
146+ if resting.remaining == 0:
147+ resting.status = OrderStatus.FILLED
148+ queue.popleft()
149+ del self._index[resting.order_id]
150+ if not queue:
151+ del opp[key]
152+ if incoming.remaining == 0:
153+ incoming.status = OrderStatus.FILLED
154+ return fills
155+
156+ def rest(self, order: Order) -> None:
157+ self._rest(order)
158+
159+ def cancel(self, order_id: int) -> Order | None:
160+ order = self._index.get(order_id)
161+ if order is None:
162+ return None
163+ self._remove_resting(order)
164+ order.status = OrderStatus.CANCELLED
165+ return order
166+
167+ def clear_all(self) -> list[Order]:
168+ orders = list(self._index.values())
169+ for o in orders:
170+ self._remove_resting(o)
171+ o.status = OrderStatus.CANCELLED
172+ return orders
173+
174+ # ---- invariants (used by tests) ------------------------------------------------------
175+ def check_invariants(self) -> None:
176+ bb, ba = self.best_bid(), self.best_ask()
177+ if bb is not None and ba is not None:
178+ assert bb < ba, f"crossed book: bid {bb} >= ask {ba}"
179+ n = 0
180+ for negp, q in self.bids.items():
181+ assert q, "empty price level"
182+ for o in q:
183+ assert o.side is Side.BUY and o.price == -negp and o.remaining > 0 and o.status is OrderStatus.OPEN
184+ n += 1
185+ for p, q in self.asks.items():
186+ assert q, "empty price level"
187+ for o in q:
188+ assert o.side is Side.SELL and o.price == p and o.remaining > 0 and o.status is OrderStatus.OPEN
189+ n += 1
190+ assert n == len(self._index), "index/book mismatch"
added src/market_agents/exchange/call_auction.py +37 −0
@@ -0,0 +1,37 @@
1+"""Periodic call auction (robustness alternative to continuous matching).
2+
3+Every `interval` ticks the engine collects the resting limit orders of a book and clears them at the
4+uniform price that maximizes executable volume (ties: the price closest to the previous mid). Market
5+orders submitted during the batch are treated as limit orders at the price band edge. Orders rest
6+between auctions without being matched continuously (the engine skips `match` while in batch mode)."""
7+from __future__ import annotations
8+
9+from .book import LimitOrderBook
10+from .orders import Order, Side
11+
12+
13+def clearing_price(book: LimitOrderBook, ref_mid: float, collar: float = 0.02) -> tuple[int | None, int]:
14+ """Uniform price maximizing executable volume among limit prices inside a collar of ±`collar` around
15+ the reference (market orders converted to band-edge limits are price takers, not candidates)."""
16+ bids = sorted(((-k, sum(o.remaining for o in q)) for k, q in book.bids.items()), reverse=True)
17+ asks = sorted(((k, sum(o.remaining for o in q)) for k, q in book.asks.items()))
18+ if not bids or not asks or bids[0][0] < asks[0][0]:
19+ return None, 0
20+ lo, hi = ref_mid * (1 - collar), ref_mid * (1 + collar)
21+ prices = sorted({p for p, _ in bids if lo <= p <= hi} | {p for p, _ in asks if lo <= p <= hi} | {int(round(ref_mid))})
22+ best, best_vol, best_dist = None, 0, float("inf")
23+ for p in prices:
24+ demand = sum(q for bp, q in bids if bp >= p)
25+ supply = sum(q for ap, q in asks if ap <= p)
26+ vol = min(demand, supply)
27+ dist = abs(p - ref_mid)
28+ if vol > best_vol or (vol == best_vol and vol > 0 and dist < best_dist):
29+ best, best_vol, best_dist = p, vol, dist
30+ return best, best_vol
31+
32+
33+def executable_orders(book: LimitOrderBook, price: int) -> tuple[list[Order], list[Order]]:
34+ """Orders that trade at `price`, in price-time priority (bids high→low, asks low→high)."""
35+ bids = [o for k, q in book.bids.items() for o in q if -k >= price]
36+ asks = [o for k, q in book.asks.items() for o in q if k <= price]
37+ return bids, asks
added src/market_agents/exchange/engine.py +487 −0
@@ -0,0 +1,487 @@
1+"""The exchange: multi-asset continuous double auction with latency, fees, risk limits, halts and an
2+append-only event log that can be replayed deterministically.
3+
4+Time is measured in ticks (float). Agents submit `Action`s at integer decision times; each intent
5+becomes an `Order` that *arrives* at submit_time + latency. The engine processes arrivals in
6+(arrival_time, seq) order, so two agents deciding at the same tick are ordered by latency and then
7+by a seeded tie-break assigned at submission.
8+"""
9+from __future__ import annotations
10+
11+import heapq
12+from dataclasses import dataclass, field
13+
14+import numpy as np
15+
16+from .accounts import Account, Ledger
17+from .book import LimitOrderBook
18+from .orders import Action, Event, Order, OrderStatus, OrderType, Side, Trade
19+
20+
21+@dataclass(slots=True)
22+class AssetSpec:
23+ asset: int
24+ name: str
25+ tick_cents: int = 1 # tick size in cents (1 => prices quoted in cents)
26+ lot: int = 1
27+ price_band_pct: float = 0.5 # orders farther than this from the reference price are rejected
28+ max_order_qty: int = 10_000
29+
30+
31+@dataclass(slots=True)
32+class ExchangeConfig:
33+ maker_fee_bps: float = 0.0 # negative = rebate
34+ taker_fee_bps: float = 2.0
35+ min_latency: float = 0.05 # ticks
36+ halt_threshold: float | None = None # |ret over window| above this triggers a halt (None = off)
37+ halt_window: int = 10
38+ halt_duration: int = 20
39+ halt_cancel_resting: bool = True
40+ allow_short: bool = False
41+ max_short_lots: int = 0
42+ margin_check: bool = True
43+ liquidation_fraction: float = 0.5 # share of the position sold on a margin call
44+ order_ttl: int | None = None # ticks after which a resting order expires (None = GTC)
45+ position_limit: int | None = None # max |position| per agent (lots)
46+ call_auction_interval: int | None = None # batch clearing every k ticks instead of continuous matching
47+ call_auction_collar: float = 0.02 # max move of the clearing price per auction (fraction of the reference)
48+
49+
50+@dataclass(slots=True)
51+class MarketState:
52+ """Public, per-asset running statistics kept by the engine for observations and metrics."""
53+ last_price: int | None = None
54+ last_trade_time: float = -1.0
55+ reference_price: int = 0
56+ halted_until: float = -1.0
57+ n_halts: int = 0
58+ volume_tick: int = 0
59+ signed_volume_tick: int = 0
60+ mid_history: list[float] = field(default_factory=list) # one entry per completed tick
61+
62+
63+class Exchange:
64+ def __init__(self, assets: list[AssetSpec], config: ExchangeConfig, rng: np.random.Generator,
65+ initial_prices: dict[int, int]) -> None:
66+ self.assets = {a.asset: a for a in assets}
67+ self.config = config
68+ self.rng = rng
69+ self.books: dict[int, LimitOrderBook] = {a: LimitOrderBook(a) for a in self.assets}
70+ self.ledger = Ledger()
71+ self.state: dict[int, MarketState] = {}
72+ for a, p0 in initial_prices.items():
73+ self.state[a] = MarketState(reference_price=p0, last_price=p0)
74+ self.events: list[Event] = []
75+ self.trades: list[Trade] = []
76+ self._queue: list[tuple[float, int, str, object]] = [] # (arrival, seq, kind, payload)
77+ self._seq = 0
78+ self._next_order_id = 1
79+ self._next_trade_id = 1
80+ self.orders: dict[int, Order] = {} # all orders ever submitted (for replay and stats)
81+ self.time: float = 0.0
82+ self.latency: dict[int, float] = {} # agent -> latency in ticks
83+ self.pending_by_agent: dict[int, set[int]] = {}
84+
85+ # ---- setup -----------------------------------------------------------------------------
86+ def register(self, acct: Account, latency: float) -> None:
87+ self.ledger.add(acct)
88+ self.latency[acct.agent_id] = max(latency, self.config.min_latency)
89+
90+ # ---- event log -------------------------------------------------------------------------
91+ def _log(self, kind: str, **payload) -> None:
92+ self._seq += 1
93+ self.events.append(Event(seq=self._seq, time=self.time, kind=kind, payload=payload))
94+
95+ # ---- public market data ----------------------------------------------------------------
96+ def marks(self) -> dict[int, float]:
97+ """Mark-to-market price in cents per lot: mid if available, else last trade, else reference."""
98+ out = {}
99+ for a in self.books:
100+ out[a] = self.mid_ticks(a) * self.assets[a].tick_cents
101+ return out
102+
103+ def mid_ticks(self, asset: int) -> float:
104+ st = self.state[asset]
105+ if self.config.call_auction_interval: # batch mode: the book may be crossed; the auction price is the reference
106+ return float(st.last_price if st.last_price is not None else st.reference_price)
107+ m = self.books[asset].mid()
108+ if m is not None:
109+ return m
110+ return float(st.last_price if st.last_price is not None else st.reference_price)
111+
112+ def spread_ticks(self, asset: int) -> int | None:
113+ if self.config.call_auction_interval:
114+ return None
115+ return self.books[asset].spread()
116+
117+ def is_halted(self, asset: int) -> bool:
118+ return self.time < self.state[asset].halted_until
119+
120+ # ---- submission --------------------------------------------------------------------------
121+ def submit(self, agent_id: int, action: Action, decision_time: float) -> list[int]:
122+ """Queue an agent's intents; returns the order ids assigned (cancels get none)."""
123+ ids: list[int] = []
124+ lat = self.latency[agent_id]
125+ for c in action.cancels:
126+ self._seq += 1
127+ heapq.heappush(self._queue, (decision_time + lat, self._seq, "cancel", (agent_id, c.order_id)))
128+ for it in action.orders:
129+ oid = self._next_order_id
130+ self._next_order_id += 1
131+ o = Order(order_id=oid, agent_id=agent_id, asset=it.asset, side=it.side, order_type=it.order_type,
132+ qty=int(it.qty), price=None if it.price is None else int(it.price),
133+ submit_time=decision_time, arrival_time=decision_time + lat, tag=it.tag)
134+ self.orders[oid] = o
135+ self.pending_by_agent.setdefault(agent_id, set()).add(oid)
136+ self._seq += 1
137+ heapq.heappush(self._queue, (o.arrival_time, self._seq, "order", o))
138+ ids.append(oid)
139+ self.ledger[agent_id].n_orders += 1
140+ return ids
141+
142+ # ---- processing --------------------------------------------------------------------------
143+ def process_until(self, t_end: float) -> None:
144+ """Process every queued arrival with arrival_time < t_end, in order."""
145+ while self._queue and self._queue[0][0] < t_end:
146+ arrival, _, kind, payload = heapq.heappop(self._queue)
147+ self.time = arrival
148+ if kind == "order":
149+ self._on_order(payload) # type: ignore[arg-type]
150+ else:
151+ agent_id, oid = payload # type: ignore[misc]
152+ self._on_cancel(agent_id, oid)
153+ self.time = t_end
154+
155+ def _fee(self, notional: int, bps: float) -> int:
156+ return int(round(notional * bps / 10_000.0))
157+
158+ def _reject(self, o: Order, reason: str) -> None:
159+ o.status = OrderStatus.REJECTED
160+ o.reject_reason = reason
161+ self.ledger[o.agent_id].n_rejects += 1
162+ self._log("reject", order_id=o.order_id, agent_id=o.agent_id, asset=o.asset, reason=reason)
163+
164+ def _on_order(self, o: Order) -> None:
165+ self.pending_by_agent.get(o.agent_id, set()).discard(o.order_id)
166+ acct = self.ledger[o.agent_id]
167+ spec = self.assets[o.asset]
168+ book = self.books[o.asset]
169+ st = self.state[o.asset]
170+ if acct.frozen and o.tag != "liquidation":
171+ return self._reject(o, "frozen")
172+ if self.is_halted(o.asset):
173+ return self._reject(o, "halted")
174+ if o.qty <= 0 or o.qty > spec.max_order_qty:
175+ return self._reject(o, "bad_qty")
176+ if o.order_type is OrderType.LIMIT:
177+ if o.price is None or o.price <= 0:
178+ return self._reject(o, "bad_price")
179+ ref = self.mid_ticks(o.asset)
180+ if abs(o.price - ref) > spec.price_band_pct * ref:
181+ return self._reject(o, "price_band")
182+ # --- capacity checks and reservations ----------------------------------------------
183+ taker_bps = self.config.taker_fee_bps
184+ if o.side is Side.BUY:
185+ if o.order_type is OrderType.LIMIT:
186+ notional = o.price * spec.tick_cents * o.qty
187+ need = notional + max(self._fee(notional, taker_bps), self._fee(notional, self.config.maker_fee_bps)) + o.qty
188+ if need > self._buying_power(acct):
189+ return self._reject(o, "insufficient_cash")
190+ acct.reserved_cash += need
191+ o.reserved_cash = need
192+ else:
193+ ba = book.best_ask()
194+ if ba is None:
195+ return self._reject(o, "no_liquidity")
196+ # cap the quantity to what free cash can pay at the worst visible price of the walk
197+ bp = self._buying_power(acct)
198+ max_q = self._max_affordable(book, o.qty, bp, spec.tick_cents, taker_bps)
199+ if max_q <= 0:
200+ return self._reject(o, "insufficient_cash")
201+ if max_q < o.qty:
202+ o.qty = max_q
203+ o.remaining = max_q
204+ else:
205+ held = acct.free_qty(o.asset)
206+ if o.qty > held:
207+ if not (acct.short_allowed and self.config.allow_short):
208+ if held <= 0:
209+ return self._reject(o, "insufficient_shares")
210+ o.qty = held
211+ o.remaining = held
212+ else:
213+ short_after = acct.positions.get(o.asset, 0) - acct.reserved_qty.get(o.asset, 0) - o.qty
214+ if -short_after > self.config.max_short_lots:
215+ return self._reject(o, "short_limit")
216+ if o.order_type is OrderType.MARKET and book.best_bid() is None:
217+ return self._reject(o, "no_liquidity")
218+ acct.reserved_qty[o.asset] = acct.reserved_qty.get(o.asset, 0) + o.qty
219+ o.reserved_qty = o.qty
220+ if self.config.position_limit is not None and o.tag != "liquidation":
221+ pos = acct.positions.get(o.asset, 0)
222+ pend = sum(x.remaining for x in self.books[o.asset].resting_for(o.agent_id) if x.side is o.side)
223+ if abs(pos + int(o.side) * (pend + o.qty)) > self.config.position_limit:
224+ self._release(o, o.remaining)
225+ return self._reject(o, "position_limit")
226+ # --- leverage check on the post-trade exposure (approximate: full fill at reference) ---
227+ if self.config.margin_check and acct.leverage_cap > 1.0 and o.tag != "liquidation":
228+ marks = self.marks()
229+ eq = acct.equity(marks)
230+ px = (o.price if o.price is not None else self.mid_ticks(o.asset)) * spec.tick_cents
231+ new_pos = acct.positions.get(o.asset, 0) + int(o.side) * o.qty
232+ gross = acct.gross_exposure(marks) - abs(acct.positions.get(o.asset, 0)) * marks[o.asset] + abs(new_pos) * px
233+ if eq <= 0 or gross > acct.leverage_cap * eq:
234+ self._release(o, o.remaining)
235+ return self._reject(o, "leverage_cap")
236+ self._log("accept", order_id=o.order_id, agent_id=o.agent_id, asset=o.asset, side=int(o.side),
237+ type=o.order_type.value, qty=o.qty, price=-1 if o.price is None else o.price, tag=o.tag)
238+ # --- match ---------------------------------------------------------------------------
239+ if self.config.call_auction_interval:
240+ if o.order_type is OrderType.MARKET:
241+ ref = self.mid_ticks(o.asset)
242+ o.price = int(ref * (1 + spec.price_band_pct * 0.99)) if o.side is Side.BUY else max(1, int(ref * (1 - spec.price_band_pct * 0.99)))
243+ o.order_type = OrderType.LIMIT
244+ o.tag = (o.tag + "|ioc") if o.tag else "ioc" # cancelled after the next auction
245+ if o.side is Side.BUY:
246+ need = o.price * spec.tick_cents * o.qty
247+ need += self._fee(need, taker_bps) + o.qty
248+ if need > self._buying_power(acct):
249+ return self._reject(o, "insufficient_cash")
250+ acct.reserved_cash += need
251+ o.reserved_cash = need
252+ book.rest(o)
253+ return
254+ fills = book.match(o)
255+ for f in fills:
256+ self._settle(f.resting, f.incoming, f.price, f.qty, aggressor=o.side)
257+ if o.remaining > 0:
258+ if o.order_type is OrderType.LIMIT:
259+ book.rest(o)
260+ else:
261+ # unfilled market remainder is cancelled (immediate-or-cancel semantics)
262+ self._release(o, o.remaining)
263+ o.status = OrderStatus.CANCELLED if o.filled > 0 else OrderStatus.REJECTED
264+ if o.filled == 0:
265+ o.reject_reason = "no_liquidity"
266+ acct.n_rejects += 1
267+ self._log("cancel", order_id=o.order_id, agent_id=o.agent_id, asset=o.asset,
268+ remaining=o.remaining, reason="ioc")
269+ self._maybe_halt(o.asset)
270+
271+ def _buying_power(self, acct: Account) -> int:
272+ if acct.leverage_cap <= 1.0:
273+ return acct.free_cash
274+ # leveraged agents may borrow against equity; limit enforced again on exposure
275+ eq = acct.equity(self.marks())
276+ return int(max(acct.free_cash, 0) + max(eq, 0) * (acct.leverage_cap - 1.0))
277+
278+ def _max_affordable(self, book: LimitOrderBook, qty: int, cash: int, tick_cents: int, bps: float) -> int:
279+ """Largest number of lots (<= qty) whose total cost — notional plus the fee on the *total* notional —
280+ fits in `cash`, walking the ask side level by level."""
281+ remaining, notional, got = qty, 0, 0
282+ for p, q in book.asks.items():
283+ level = sum(o.remaining for o in q)
284+ take = min(level, remaining)
285+ for _ in range(take):
286+ n2 = notional + p * tick_cents
287+ if n2 + self._fee(n2, bps) > cash:
288+ return got
289+ notional = n2
290+ got += 1
291+ remaining -= take
292+ if remaining == 0:
293+ break
294+ return got
295+
296+ def _release(self, o: Order, qty: int) -> None:
297+ """Release whatever reservation is still attached to order `o` (called when it leaves the book)."""
298+ acct = self.ledger[o.agent_id]
299+ if o.side is Side.BUY and o.reserved_cash:
300+ acct.reserved_cash -= o.reserved_cash
301+ o.reserved_cash = 0
302+ elif o.side is Side.SELL and o.reserved_qty:
303+ acct.reserved_qty[o.asset] -= o.reserved_qty
304+ o.reserved_qty = 0
305+
306+ def _settle(self, resting: Order, incoming: Order, price: int, qty: int, aggressor: Side) -> None:
307+ spec = self.assets[resting.asset]
308+ notional = price * spec.tick_cents * qty
309+ maker_fee = self._fee(notional, self.config.maker_fee_bps)
310+ taker_fee = self._fee(notional, self.config.taker_fee_bps)
311+ if aggressor is Side.BUY:
312+ buy, sell = incoming, resting
313+ buyer_fee, seller_fee = taker_fee, maker_fee
314+ else:
315+ buy, sell = resting, incoming
316+ buyer_fee, seller_fee = maker_fee, taker_fee
317+ # reservation to release on each side for this quantity: exactly what is charged, everything on the last fill
318+ if buy.reserved_cash:
319+ buy_rel = buy.reserved_cash if buy.remaining == 0 else min(buy.reserved_cash, notional + buyer_fee)
320+ else:
321+ buy_rel = 0
322+ sell_rel = min(qty, sell.reserved_qty) if sell.reserved_qty else 0
323+ self.ledger.apply_fill(buy.agent_id, sell.agent_id, resting.asset, price * spec.tick_cents, qty,
324+ buyer_fee, seller_fee, buy_rel, sell_rel)
325+ buy.reserved_cash -= buy_rel
326+ sell.reserved_qty -= sell_rel
327+ tr = Trade(trade_id=self._next_trade_id, time=self.time, asset=resting.asset, price=price, qty=qty,
328+ buy_order_id=buy.order_id, sell_order_id=sell.order_id, buyer_id=buy.agent_id,
329+ seller_id=sell.agent_id, aggressor=aggressor, maker_fee=maker_fee, taker_fee=taker_fee)
330+ self._next_trade_id += 1
331+ self.trades.append(tr)
332+ st = self.state[resting.asset]
333+ st.last_price = price
334+ st.last_trade_time = self.time
335+ st.volume_tick += qty
336+ st.signed_volume_tick += int(aggressor) * qty
337+ self._log("trade", trade_id=tr.trade_id, asset=tr.asset, price=price, qty=qty, buyer=buy.agent_id,
338+ seller=sell.agent_id, aggressor=int(aggressor), buy_order=buy.order_id, sell_order=sell.order_id,
339+ maker_fee=maker_fee, taker_fee=taker_fee)
340+ self.ledger.check_conservation()
341+
342+ def _on_cancel(self, agent_id: int, order_id: int) -> None:
343+ o = self.orders.get(order_id)
344+ if o is None or o.agent_id != agent_id:
345+ return
346+ book = self.books[o.asset]
347+ if order_id in book:
348+ book.cancel(order_id)
349+ self._release(o, o.remaining)
350+ self.ledger[agent_id].n_cancels += 1
351+ self._log("cancel", order_id=order_id, agent_id=agent_id, asset=o.asset, remaining=o.remaining,
352+ reason="user")
353+
354+ # ---- halts -----------------------------------------------------------------------------
355+ def _maybe_halt(self, asset: int) -> None:
356+ thr = self.config.halt_threshold
357+ if thr is None:
358+ return
359+ st = self.state[asset]
360+ hist = st.mid_history
361+ if len(hist) < self.config.halt_window or st.last_price is None:
362+ return
363+ ref = hist[-self.config.halt_window]
364+ if ref <= 0:
365+ return
366+ if abs(st.last_price / ref - 1.0) > thr and not self.is_halted(asset):
367+ st.halted_until = self.time + self.config.halt_duration
368+ st.n_halts += 1
369+ self._log("halt", asset=asset, until=st.halted_until, move=st.last_price / ref - 1.0)
370+ if self.config.halt_cancel_resting:
371+ for o in self.books[asset].clear_all():
372+ self._release(o, o.remaining)
373+ self._log("cancel", order_id=o.order_id, agent_id=o.agent_id, asset=asset,
374+ remaining=o.remaining, reason="halt")
375+
376+ # ---- end-of-tick housekeeping --------------------------------------------------------------
377+ def run_call_auction(self, asset: int) -> int:
378+ from .call_auction import clearing_price, executable_orders
379+ book = self.books[asset]
380+ price, vol = clearing_price(book, self.mid_ticks(asset), self.config.call_auction_collar)
381+ if price is None or vol == 0:
382+ return 0
383+ bids, asks = executable_orders(book, price)
384+ done = 0
385+ bi = ai = 0
386+ while done < vol and bi < len(bids) and ai < len(asks):
387+ b, a = bids[bi], asks[ai]
388+ q = min(b.remaining, a.remaining, vol - done)
389+ b.remaining -= q
390+ a.remaining -= q
391+ # both sides are resting: treat the later-submitted order as the aggressor for fee purposes
392+ aggressor = Side.BUY if b.submit_time >= a.submit_time else Side.SELL
393+ self._settle(a if aggressor is Side.BUY else b, b if aggressor is Side.BUY else a, price, q, aggressor)
394+ done += q
395+ for o in (b, a):
396+ if o.remaining == 0:
397+ book.cancel(o.order_id)
398+ o.status = OrderStatus.FILLED
399+ self._release(o, 0)
400+ if b.remaining == 0:
401+ bi += 1
402+ if a.remaining == 0:
403+ ai += 1
404+ self._log("auction", asset=asset, price=price, volume=done)
405+ for o in [x for x in book.resting_orders() if "ioc" in x.tag]: # market orders do not carry over
406+ book.cancel(o.order_id)
407+ self._release(o, o.remaining)
408+ self._log("cancel", order_id=o.order_id, agent_id=o.agent_id, asset=asset, remaining=o.remaining, reason="ioc")
409+ return done
410+
411+ def end_of_tick(self, t: int) -> None:
412+ if self.config.call_auction_interval and (t + 1) % self.config.call_auction_interval == 0:
413+ for a in self.books:
414+ self.run_call_auction(a)
415+ for a, st in self.state.items():
416+ st.mid_history.append(self.mid_ticks(a))
417+ st.volume_tick = 0
418+ st.signed_volume_tick = 0
419+ if self.config.order_ttl is not None:
420+ for a, book in self.books.items():
421+ expired = [o for o in book.resting_orders() if t - o.submit_time >= self.config.order_ttl]
422+ for o in expired:
423+ book.cancel(o.order_id)
424+ self._release(o, o.remaining)
425+ self._log("expire", order_id=o.order_id, agent_id=o.agent_id, asset=a, remaining=o.remaining)
426+ self._log("tick", t=t, **{f"mid_{a}": self.mid_ticks(a) for a in self.books})
427+
428+ def pay_dividends(self, asset: int, per_lot_cents: int) -> None:
429+ paid = self.ledger.pay_dividend(asset, per_lot_cents)
430+ self._log("dividend", asset=asset, per_lot_cents=per_lot_cents, paid=paid)
431+ self.ledger.check_conservation()
432+
433+ def margin_check(self) -> list[Action]:
434+ """Returns liquidation actions for agents violating margin; freezes bankrupt agents."""
435+ if not self.config.margin_check:
436+ return []
437+ marks = self.marks()
438+ actions: list[tuple[int, Action]] = []
439+ for acct in self.ledger.accounts.values():
440+ if acct.frozen:
441+ continue
442+ eq = acct.equity(marks)
443+ gross = acct.gross_exposure(marks)
444+ if gross <= 0:
445+ continue
446+ if eq <= 0:
447+ acct.frozen = True
448+ self._log("bankrupt", agent_id=acct.agent_id, equity=eq)
449+ self._liquidate_all(acct, actions, marks)
450+ continue
451+ if acct.leverage_cap > 1.0 and eq / gross < acct.maintenance_margin:
452+ self._log("margin_call", agent_id=acct.agent_id, equity=eq, gross=gross)
453+ self._liquidate(acct, actions, self.config.liquidation_fraction)
454+ return actions # type: ignore[return-value]
455+
456+ def _liquidate(self, acct: Account, actions: list, fraction: float) -> None:
457+ from .orders import OrderIntent
458+ act = Action()
459+ for a, book in self.books.items():
460+ for o in book.resting_for(acct.agent_id):
461+ from .orders import CancelIntent
462+ act.cancels.append(CancelIntent(o.order_id))
463+ for a, q in acct.positions.items():
464+ if q == 0:
465+ continue
466+ n = max(1, int(abs(q) * fraction))
467+ act.orders.append(OrderIntent(asset=a, side=Side.SELL if q > 0 else Side.BUY,
468+ order_type=OrderType.MARKET, qty=n, tag="liquidation"))
469+ if not act.is_hold:
470+ self._log("liquidate", agent_id=acct.agent_id, n_orders=len(act.orders))
471+ actions.append((acct.agent_id, act))
472+
473+ def _liquidate_all(self, acct: Account, actions: list, marks: dict[int, float]) -> None:
474+ self._liquidate(acct, actions, 1.0)
475+
476+ # ---- replay ----------------------------------------------------------------------------
477+ def snapshot(self) -> dict:
478+ """Hashable summary of the whole state (used by determinism and replay tests)."""
479+ return {
480+ "time": self.time,
481+ "cash": {a: acc.cash for a, acc in sorted(self.ledger.accounts.items())},
482+ "pos": {a: dict(sorted(acc.positions.items())) for a, acc in sorted(self.ledger.accounts.items())},
483+ "fees": self.ledger.exchange_fees,
484+ "books": {a: self.books[a].depth(1000) for a in self.books},
485+ "n_trades": len(self.trades),
486+ "n_events": len(self.events),
487+ }
added src/market_agents/exchange/orders.py +133 −0
@@ -0,0 +1,133 @@
1+"""Core typed records for the exchange.
2+
3+Conventions (exact integer arithmetic everywhere):
4+- prices are integers in *ticks* (tick_size in currency units is a property of the asset);
5+- quantities are integers in *lots*;
6+- cash is an integer in *cents* (1 currency unit = 100 cents); a trade's notional in cents is
7+ price_ticks * tick_cents * qty, where tick_cents = tick_size * 100 must be an integer.
8+"""
9+from __future__ import annotations
10+
11+from dataclasses import dataclass, field
12+from enum import Enum, IntEnum
13+from typing import Literal
14+
15+
16+class Side(IntEnum):
17+ BUY = 1
18+ SELL = -1
19+
20+ @property
21+ def opposite(self) -> "Side":
22+ return Side.SELL if self is Side.BUY else Side.BUY
23+
24+ @property
25+ def label(self) -> str:
26+ return "buy" if self is Side.BUY else "sell"
27+
28+
29+class OrderType(str, Enum):
30+ LIMIT = "limit"
31+ MARKET = "market"
32+
33+
34+class OrderStatus(str, Enum):
35+ PENDING = "pending" # in the latency queue, not yet at the exchange
36+ OPEN = "open" # resting in the book (possibly partially filled)
37+ FILLED = "filled"
38+ CANCELLED = "cancelled" # cancelled by the owner (possibly after partial fills)
39+ REJECTED = "rejected" # never entered the book
40+
41+
42+@dataclass(slots=True)
43+class Order:
44+ order_id: int
45+ agent_id: int
46+ asset: int
47+ side: Side
48+ order_type: OrderType
49+ qty: int # original quantity (lots)
50+ price: int | None # limit price in ticks (None for market orders)
51+ submit_time: float # agent decision time (ticks)
52+ arrival_time: float # exchange arrival time = submit_time + latency
53+ remaining: int = 0
54+ status: OrderStatus = OrderStatus.PENDING
55+ reserved_cash: int = 0 # cents reserved at acceptance (bids)
56+ reserved_qty: int = 0 # lots reserved at acceptance (asks)
57+ tag: str = "" # free-form (policy name, 'liquidation', ...)
58+ reject_reason: str = ""
59+
60+ def __post_init__(self) -> None:
61+ if self.remaining == 0 and self.status is OrderStatus.PENDING:
62+ self.remaining = self.qty
63+
64+ @property
65+ def filled(self) -> int:
66+ return self.qty - self.remaining
67+
68+
69+@dataclass(slots=True)
70+class Trade:
71+ trade_id: int
72+ time: float
73+ asset: int
74+ price: int # ticks (the resting order's price)
75+ qty: int
76+ buy_order_id: int
77+ sell_order_id: int
78+ buyer_id: int
79+ seller_id: int
80+ aggressor: Side # side of the incoming (taker) order
81+ maker_fee: int # cents paid by the maker (may be negative = rebate)
82+ taker_fee: int # cents paid by the taker
83+
84+ @property
85+ def taker_id(self) -> int:
86+ return self.buyer_id if self.aggressor is Side.BUY else self.seller_id
87+
88+ @property
89+ def maker_id(self) -> int:
90+ return self.seller_id if self.aggressor is Side.BUY else self.buyer_id
91+
92+
93+EventKind = Literal[
94+ "submit", "accept", "reject", "trade", "cancel", "expire", "halt", "resume",
95+ "dividend", "margin_call", "liquidate", "bankrupt", "tick", "auction",
96+]
97+
98+
99+@dataclass(slots=True)
100+class Event:
101+ """One row of the event-sourced log. `payload` is a flat dict of scalars (parquet-friendly)."""
102+ seq: int
103+ time: float
104+ kind: str
105+ payload: dict = field(default_factory=dict)
106+
107+
108+@dataclass(slots=True)
109+class OrderIntent:
110+ """What an agent *wants* to do; converted to an Order by the exchange on arrival."""
111+ asset: int
112+ side: Side
113+ order_type: OrderType
114+ qty: int
115+ price: int | None = None # ticks
116+ tag: str = ""
117+
118+
119+@dataclass(slots=True)
120+class CancelIntent:
121+ order_id: int
122+
123+
124+@dataclass(slots=True)
125+class Action:
126+ """A bundle of intents emitted by one agent at one decision time."""
127+ orders: list[OrderIntent] = field(default_factory=list)
128+ cancels: list[CancelIntent] = field(default_factory=list)
129+ meta: dict = field(default_factory=dict) # e.g. LLM rationale/confidence/validity flags
130+
131+ @property
132+ def is_hold(self) -> bool:
133+ return not self.orders and not self.cancels
added src/market_agents/experiments/__init__.py +0 −0
added src/market_agents/experiments/config.py +202 −0
@@ -0,0 +1,202 @@
1+"""Configuration schema (pydantic) for one simulation run. Loaded from YAML; hashed into manifests."""
2+from __future__ import annotations
3+
4+import hashlib
5+import json
6+from pathlib import Path
7+from typing import Any
8+
9+import yaml
10+from pydantic import BaseModel, Field, model_validator
11+
12+
13+class AssetsCfg(BaseModel):
14+ n_assets: int = 1
15+ tick_cents: int = 1
16+ initial_price_cents: int = 20_000 # 200.00 per lot; chosen to equal mu/r (= 50/0.0025) by default
17+ price_band_pct: float = 0.5
18+ max_order_qty: int = 10_000
19+
20+
21+class FundamentalsCfg(BaseModel):
22+ div_interval: int = 20
23+ r_period: float = 0.0025
24+ mu: list[float] = [50.0]
25+ mu_shift: float = 0.0
26+ p_switch: float = 0.0
27+ rho: float = 0.98
28+ sigma: float = 2.0
29+ jump_prob: float = 0.0
30+ jump_sigma: float = 0.0
31+ factor_loading: list[float] = [0.0]
32+ x0: float = 0.0
33+
34+
35+class NewsCfg(BaseModel):
36+ rate: float = 0.02
37+ noise_sd: float = 1.0
38+ misleading_share: float = 0.0
39+ misleading_bias: float = 10.0
40+ window: int = 20
41+
42+
43+class ExchangeCfg(BaseModel):
44+ maker_fee_bps: float = 0.0
45+ taker_fee_bps: float = 2.0
46+ min_latency: float = 0.05
47+ halt_threshold: float | None = None
48+ halt_window: int = 10
49+ halt_duration: int = 20
50+ halt_cancel_resting: bool = True
51+ allow_short: bool = False
52+ max_short_lots: int = 0
53+ margin_check: bool = True
54+ liquidation_fraction: float = 0.5
55+ order_ttl: int | None = 50
56+ position_limit: int | None = None # max |position| in lots per agent (safeguard treatment)
57+ call_auction_interval: int | None = None # if set, batch orders and clear at a uniform price every k ticks (robustness)
58+
59+
60+class EndowmentCfg(BaseModel):
61+ cash_cents: int = 2_000_000 # 20,000.00
62+ shares: int = 100
63+ wealth_dispersion: float = 0.3 # lognormal sd of the wealth multiplier across traders
64+
65+
66+class LatencyCfg(BaseModel):
67+ min: float = 0.05
68+ max: float = 1.0
69+ llm_latency: float | None = None # matched-latency design: LLM agents get this latency (None = drawn like others)
70+
71+
72+class LLMCfg(BaseModel):
73+ provider_mix: dict[str, float] = {"mock": 1.0} # provider -> share of LLM slots
74+ models: dict[str, str] = {"mock": "mock-v1", "openai": "gpt-5.4-nano", "anthropic": "claude-haiku-5-5"}
75+ prompt_set: str = "single" # 'single' | 'diverse'
76+ prompt_version: str = "v1"
77+ memory: int = 0 # number of past own-decision summaries shown (0 = none)
78+ channel: bool = False # cohort communication channel
79+ cohort_size: int = 5
80+ decision_interval: int = 20
81+ stagger: bool = True # spread decision offsets across the interval
82+ stagger_slots: int = 4 # number of distinct offsets (batches of agents decide together -> parallel API calls)
83+ informed_share: float | None = None # None: an LLM agent inherits the information set of the slot it replaces (default, keeps
84+ # information fixed within matched pairs); a number overrides it (information-equalization arm)
85+ precision: float = 2.0
86+ max_order_qty: int = 50
87+ temperature: float | None = None
88+ reasoning_effort: str | None = "low" # openai gpt-5 family; anthropic 'effort'
89+ max_output_tokens: int = 1200
90+ concurrency: int = 8
91+ timeout_s: float = 60.0
92+ max_retries: int = 3
93+ usd_cap: float = 5.0 # hard cap for this run (USD)
94+ token_cap: int | None = None
95+ decision_cap: int | None = None # max decision opportunities per agent (fairness design)
96+ cache: bool = True # reuse identical (provider, model, prompt) decisions
97+ frozen_library: str | None = None # path to a recorded decision library for offline replay
98+ adaptive_prompt: bool = False # include P&L feedback line in the prompt
99+ price_assumptions: dict[str, dict[str, float]] = {
100+ # USD per 1M tokens: input, cached input, output — see docs/model_cards.md for sources and dates
101+ "gpt-5.4-nano": {"input": 0.20, "cached": 0.02, "output": 1.25},
102+ "gpt-5.4-mini": {"input": 0.75, "cached": 0.075, "output": 4.50},
103+ "gpt-5-nano": {"input": 0.05, "cached": 0.005, "output": 0.40},
104+ "gpt-5-mini": {"input": 0.25, "cached": 0.025, "output": 2.00},
105+ "gpt-4.1-nano": {"input": 0.10, "cached": 0.025, "output": 0.40},
106+ "gpt-4.1-mini": {"input": 0.40, "cached": 0.10, "output": 1.60},
107+ "claude-haiku-5-5": {"input": 0.10, "cached": 0.01, "output": 0.50},
108+ "claude-sonnet-5-5": {"input": 2.00, "cached": 0.20, "output": 10.00},
109+ "mock-v1": {"input": 0.0, "cached": 0.0, "output": 0.0},
110+ }
111+
112+
113+class PopulationCfg(BaseModel):
114+ n_traders: int = 100
115+ mix: dict[str, float] = {"fundamentalist": 0.25, "trend": 0.15, "meanrev": 0.10, "noise": 0.35,
116+ "market_maker": 0.05, "statarb": 0.0, "leveraged": 0.10}
117+ activity: dict[str, float] = {"fundamentalist": 0.20, "trend": 0.20, "meanrev": 0.20, "noise": 0.30,
118+ "market_maker": 0.5, "statarb": 0.2, "leveraged": 0.2}
119+ policy_kwargs: dict[str, dict[str, Any]] = {}
120+ llm_share: float = 0.0 # fraction of trader *slots* controlled by LLM policies
121+ llm_replace: str = "proportional" # 'proportional' (stratified across types) or a policy name
122+ llm_keep_market_makers: bool = True # never replace market makers unless llm_replace == 'market_maker'
123+ informed_share: float = 0.3 # share of fundamentalists receiving private signals
124+ precision: float = 2.0
125+ leverage_cap: float = 3.0 # for 'leveraged' slots
126+ maintenance_margin: float = 0.25
127+ endowment: EndowmentCfg = EndowmentCfg()
128+ latency: LatencyCfg = LatencyCfg()
129+ llm: LLMCfg = LLMCfg()
130+
131+ @model_validator(mode="after")
132+ def _check(self) -> "PopulationCfg":
133+ s = sum(self.mix.values())
134+ if abs(s - 1.0) > 1e-6:
135+ raise ValueError(f"population.mix must sum to 1 (got {s})")
136+ if not 0.0 <= self.llm_share <= 1.0:
137+ raise ValueError("llm_share must be in [0,1]")
138+ return self
139+
140+
141+class ShockCfg(BaseModel):
142+ time: int
143+ kind: str # fundamental_jump | rate | funding | liquidity | placebo | news_burst
144+ size: float = 0.0
145+ asset: int | None = None
146+ duration: int = 0
147+
148+
149+class OutputCfg(BaseModel):
150+ events: bool = False # write the full event log (large)
151+ trades: bool = True
152+ llm_log: bool = True
153+ lookback: int = 60 # ticks of mid history visible to agents
154+
155+
156+class RunConfig(BaseModel):
157+ name: str = "run"
158+ seed: int = 1
159+ n_ticks: int = 500
160+ warmup: int = 50 # ticks excluded from outcome metrics
161+ assets: AssetsCfg = AssetsCfg()
162+ fundamentals: FundamentalsCfg = FundamentalsCfg()
163+ news: NewsCfg = NewsCfg()
164+ exchange: ExchangeCfg = ExchangeCfg()
165+ population: PopulationCfg = PopulationCfg()
166+ shocks: list[ShockCfg] = Field(default_factory=list)
167+ output: OutputCfg = OutputCfg()
168+ treatment: dict[str, Any] = Field(default_factory=dict) # free-form labels copied to the manifest
169+ notes: str = ""
170+
171+ # ---- helpers ---------------------------------------------------------------------------
172+ def config_hash(self) -> str:
173+ payload = json.dumps(self.model_dump(mode="json"), sort_keys=True).encode()
174+ return hashlib.sha256(payload).hexdigest()[:16]
175+
176+ @classmethod
177+ def from_yaml(cls, path: str | Path, **overrides: Any) -> "RunConfig":
178+ with open(path) as f:
179+ raw = yaml.safe_load(f) or {}
180+ raw = deep_update(raw, overrides)
181+ return cls.model_validate(raw)
182+
183+ def with_overrides(self, **overrides: Any) -> "RunConfig":
184+ raw = deep_update(self.model_dump(mode="json"), overrides)
185+ return RunConfig.model_validate(raw)
186+
187+ def to_yaml(self, path: str | Path) -> None:
188+ with open(path, "w") as f:
189+ yaml.safe_dump(self.model_dump(mode="json"), f, sort_keys=False)
190+
191+
192+def deep_update(base: dict, upd: dict) -> dict:
193+ out = dict(base)
194+ for k, v in upd.items():
195+ if "." in k: # dotted override: population.llm_share=0.25
196+ head, rest = k.split(".", 1)
197+ out[head] = deep_update(out.get(head, {}) or {}, {rest: v})
198+ elif isinstance(v, dict) and isinstance(out.get(k), dict):
199+ out[k] = deep_update(out[k], v)
200+ else:
201+ out[k] = v
202+ return out
added src/market_agents/experiments/design.py +111 −0
@@ -0,0 +1,111 @@
1+"""Experimental designs: a design = list of cells (config overrides + treatment labels) × seeds.
2+Runs are executed with multiprocessing (classical) or sequentially with a shared LLM cache (API cells),
3+each writing its own run directory + manifest; outcomes are appended to a parquet table."""
4+from __future__ import annotations
5+
6+import json
7+import time
8+from concurrent.futures import ProcessPoolExecutor, as_completed
9+from pathlib import Path
10+
11+import pandas as pd
12+import yaml
13+
14+from ..metrics.outcomes import run_outcomes
15+from .config import RunConfig
16+from .runner import run_config
17+
18+
19+def load_design(path: str | Path) -> dict:
20+ with open(path) as f:
21+ return yaml.safe_load(f)
22+
23+
24+def expand_cells(design: dict) -> list[dict]:
25+ """Design file: {base: <config path or dict>, seeds: [..] | {start, n}, cells: [{name, overrides, treatment}], factors: {...}}.
26+ `factors` (optional) is a dict factor -> list of {label, overrides}; the full factorial product is expanded."""
27+ cells = list(design.get("cells", []))
28+ factors = design.get("factors")
29+ if factors:
30+ import itertools
31+ names = list(factors)
32+ for combo in itertools.product(*[factors[n] for n in names]):
33+ ov, tr, label = {}, {}, []
34+ for n, lev in zip(names, combo):
35+ ov.update(lev.get("overrides", {}))
36+ tr[n] = lev["label"]
37+ label.append(f"{n}={lev['label']}")
38+ cells.append({"name": "__".join(label), "overrides": ov, "treatment": tr})
39+ return cells
40+
41+
42+def seeds_of(design: dict) -> list[int]:
43+ s = design.get("seeds", {"start": 1, "n": 30})
44+ if isinstance(s, list):
45+ return [int(x) for x in s]
46+ return list(range(int(s.get("start", 1)), int(s.get("start", 1)) + int(s["n"])))
47+
48+
49+def cell_config(design: dict, cell: dict, seed: int, base_dir: Path) -> RunConfig:
50+ base = design["base"]
51+ cfg = RunConfig.from_yaml(base_dir / base) if isinstance(base, str) else RunConfig.model_validate(base)
52+ ov = dict(design.get("overrides", {}))
53+ ov.update(cell.get("overrides", {}))
54+ cfg = cfg.with_overrides(**ov)
55+ return cfg.with_overrides(name=f"{design['name']}/{cell['name']}", seed=seed,
56+ treatment={**design.get("treatment", {}), **cell.get("treatment", {})})
57+
58+
59+def _run_one(args):
60+ cfg_json, out_dir, cache_path = args
61+ cfg = RunConfig.model_validate(cfg_json)
62+ out = Path(out_dir)
63+ if (out / "outcomes.json").exists():
64+ with open(out / "outcomes.json") as f:
65+ return json.load(f)
66+ res = run_config(cfg, out_dir=out, cache_path=cache_path)
67+ oc = run_outcomes(res)
68+ oc["run_dir"] = str(out)
69+ with open(out / "outcomes.json", "w") as f:
70+ json.dump(oc, f, indent=1, default=float)
71+ return oc
72+
73+
74+def run_design(design_path: str | Path, results_root: str | Path = "results/runs", workers: int = 4,
75+ cache_path: str | None = None, only_cells: list[str] | None = None, max_seeds: int | None = None,
76+ progress: bool = True) -> pd.DataFrame:
77+ design_path = Path(design_path)
78+ design = load_design(design_path)
79+ cells = expand_cells(design)
80+ if only_cells:
81+ cells = [c for c in cells if c["name"] in only_cells]
82+ seeds = seeds_of(design)
83+ if max_seeds:
84+ seeds = seeds[:max_seeds]
85+ root = Path(results_root) / design["name"]
86+ root.mkdir(parents=True, exist_ok=True)
87+ jobs = []
88+ for c in cells:
89+ for s in seeds:
90+ cfg = cell_config(design, c, s, design_path.parent)
91+ out = root / c["name"] / f"seed{s:04d}"
92+ jobs.append((cfg.model_dump(mode="json"), str(out), cache_path))
93+ t0 = time.perf_counter()
94+ rows = []
95+ if workers <= 1:
96+ for i, j in enumerate(jobs):
97+ rows.append(_run_one(j))
98+ if progress:
99+ print(f" [{i + 1}/{len(jobs)}] {j[1]} ({time.perf_counter() - t0:.0f}s)", flush=True)
100+ else:
101+ with ProcessPoolExecutor(max_workers=workers) as ex:
102+ futs = {ex.submit(_run_one, j): j for j in jobs}
103+ for i, f in enumerate(as_completed(futs)):
104+ rows.append(f.result())
105+ if progress and (i + 1) % 10 == 0:
106+ print(f" [{i + 1}/{len(jobs)}] done ({time.perf_counter() - t0:.0f}s)", flush=True)
107+ df = pd.DataFrame(rows)
108+ df.to_parquet(root / "outcomes.parquet", index=False)
109+ with open(root / "design.json", "w") as f:
110+ json.dump({"design": design, "cells": cells, "seeds": seeds, "n_runs": len(jobs), "wall_s": time.perf_counter() - t0}, f, indent=1, default=str)
111+ return df
added src/market_agents/experiments/population.py +119 −0
@@ -0,0 +1,119 @@
1+"""Builds the trader population for a run: randomized assignment of policies to trader slots,
2+stratified by wealth, information and latency (see docs/identification.md).
3+
4+Assignment is a function of (seed, config) only, so matched pairs that differ in `llm_share`
5+share the same slot endowments, latencies and signal precisions: the LLM treatment *replaces* the
6+policy in a subset of slots, chosen by a dedicated RNG stream, holding everything else fixed."""
7+from __future__ import annotations
8+
9+import numpy as np
10+
11+from ..agents.base import AgentSpec
12+from .config import RunConfig
13+
14+
15+def _stratum(wealth_mult: float, precision: float, latency: float) -> str:
16+ w = "rich" if wealth_mult > 1.0 else "poor"
17+ i = "informed" if precision > 0 else "uninformed"
18+ l_ = "fast" if latency < 0.3 else "slow"
19+ return f"{w}/{i}/{l_}"
20+
21+
22+def build_population(cfg: RunConfig, seed_streams: dict[str, np.random.Generator]) -> list[AgentSpec]:
23+ pop = cfg.population
24+ N = pop.n_traders
25+ rng_slots = seed_streams["slots"] # endowments, latencies, information
26+ rng_assign = seed_streams["assign"] # policy assignment to slots
27+ rng_llm = seed_streams["llm_assign"] # which slots become LLM-controlled
28+
29+ # 1. slot characteristics (independent of policies)
30+ wealth = np.exp(rng_slots.standard_normal(N) * pop.endowment.wealth_dispersion)
31+ wealth /= wealth.mean()
32+ latency = rng_slots.uniform(pop.latency.min, pop.latency.max, N)
33+ informed_draw = rng_slots.random(N)
34+
35+ # 2. classical policy for each slot (stratified: sort slots by wealth tercile and deal types evenly)
36+ names = list(pop.mix)
37+ counts = np.floor(np.array([pop.mix[n] for n in names]) * N).astype(int)
38+ while counts.sum() < N:
39+ counts[int(np.argmax(np.array([pop.mix[n] for n in names]) * N - counts))] += 1
40+ deck = np.concatenate([np.full(c, i) for i, c in enumerate(counts)])
41+ rng_assign.shuffle(deck)
42+ order = np.argsort(wealth) # stratify by wealth: deal the shuffled deck down the wealth ranking
43+ policy_idx = np.empty(N, dtype=int)
44+ policy_idx[order] = deck
45+
46+ # 3. LLM slots: replace a fraction of the (non market-maker) slots, stratified by policy
47+ n_llm = int(round(pop.llm_share * N))
48+ eligible = [i for i in range(N) if not (pop.llm_keep_market_makers and names[policy_idx[i]] == "market_maker"
49+ and pop.llm_replace != "market_maker")]
50+ if pop.llm_replace != "proportional":
51+ eligible = [i for i in eligible if names[policy_idx[i]] == pop.llm_replace]
52+ n_llm = min(n_llm, len(eligible))
53+ llm_slots: set[int] = set()
54+ if n_llm > 0:
55+ # stratified draw: proportional within each policy type, remainder at random
56+ by_type: dict[int, list[int]] = {}
57+ for i in eligible:
58+ by_type.setdefault(int(policy_idx[i]), []).append(i)
59+ quota = {k: int(np.floor(n_llm * len(v) / len(eligible))) for k, v in by_type.items()}
60+ for k, v in by_type.items():
61+ chosen = rng_llm.choice(v, size=min(quota[k], len(v)), replace=False)
62+ llm_slots.update(int(c) for c in chosen)
63+ rest = [i for i in eligible if i not in llm_slots]
64+ extra = n_llm - len(llm_slots)
65+ if extra > 0 and rest:
66+ llm_slots.update(int(c) for c in rng_llm.choice(rest, size=min(extra, len(rest)), replace=False))
67+
68+ # 4. LLM provider / prompt / cohort assignment
69+ llm = pop.llm
70+ providers = list(llm.provider_mix)
71+ pw = np.array([llm.provider_mix[p] for p in providers], dtype=float)
72+ pw = pw / pw.sum()
73+ llm_list = sorted(llm_slots)
74+ prov_of: dict[int, str] = {}
75+ if llm_list:
76+ # deterministic proportional allocation then shuffle
77+ alloc = np.floor(pw * len(llm_list)).astype(int)
78+ while alloc.sum() < len(llm_list):
79+ alloc[int(np.argmax(pw * len(llm_list) - alloc))] += 1
80+ plist = np.concatenate([np.full(c, i) for i, c in enumerate(alloc)])
81+ rng_llm.shuffle(plist)
82+ prov_of = {s: providers[int(p)] for s, p in zip(llm_list, plist)}
83+ prompts = ["value", "momentum", "contrarian", "liquidity", "risk_averse", "macro", "quant", "generalist"]
84+ slots = max(1, min(llm.stagger_slots, llm.decision_interval))
85+ offsets = (rng_llm.integers(0, slots, size=max(len(llm_list), 1)) * llm.decision_interval) // slots
86+ cohort_of = {s: (k // max(llm.cohort_size, 1)) for k, s in enumerate(llm_list)} if llm.channel else {}
87+
88+ specs: list[AgentSpec] = []
89+ for i in range(N):
90+ pname = names[policy_idx[i]]
91+ kw = dict(pop.policy_kwargs.get(pname, {}))
92+ prec = 0.0
93+ if pname == "fundamentalist" and informed_draw[i] < pop.informed_share:
94+ prec = pop.precision
95+ lev = pop.leverage_cap if pname == "leveraged" else 1.0
96+ cash = int(pop.endowment.cash_cents * wealth[i])
97+ shares = int(round(pop.endowment.shares * wealth[i]))
98+ positions = {a: shares for a in range(cfg.assets.n_assets)}
99+ if pname == "market_maker":
100+ cash *= 5
101+ positions = {a: shares * 5 for a in range(cfg.assets.n_assets)}
102+ if i in llm_slots:
103+ prov = prov_of[i]
104+ k = llm_list.index(i)
105+ prompt_id = prompts[k % len(prompts)] if llm.prompt_set == "diverse" else "generalist"
106+ llm_prec = prec if llm.informed_share is None else (llm.precision if informed_draw[i] < llm.informed_share else 0.0)
107+ specs.append(AgentSpec(
108+ agent_id=i, policy_name="llm", policy_kwargs={"replaced": pname}, cash=cash, positions=positions,
109+ latency=float(latency[i] if pop.latency.llm_latency is None else pop.latency.llm_latency),
110+ activity=1.0, decision_interval=llm.decision_interval,
111+ decision_offset=int(offsets[k]) if llm.stagger else 0, signal_precision=llm_prec,
112+ leverage_cap=lev, short_allowed=False, stratum=_stratum(wealth[i], llm_prec, latency[i]),
113+ provider=prov, model=llm.models.get(prov, ""), prompt_id=prompt_id, cohort=cohort_of.get(i, -1)))
114+ else:
115+ specs.append(AgentSpec(
116+ agent_id=i, policy_name=pname, policy_kwargs=kw, cash=cash, positions=positions,
117+ latency=float(latency[i]), activity=pop.activity.get(pname, 0.1), signal_precision=prec,
118+ leverage_cap=lev, short_allowed=False, stratum=_stratum(wealth[i], prec, latency[i])))
119+ return specs
added src/market_agents/experiments/runner.py +357 −0
@@ -0,0 +1,357 @@
1+"""One simulation run: build population, loop over ticks, record series, write artefacts."""
2+from __future__ import annotations
3+
4+import json
5+import time
6+from collections import deque
7+from dataclasses import dataclass, field
8+from pathlib import Path
9+
10+import numpy as np
11+import pandas as pd
12+
13+from .. import __version__
14+from ..agents.base import AgentSpec, BookView, Observation, OwnOrderView
15+from ..agents.classical import CLASSICAL_POLICIES
16+from ..assets.fundamentals import FundamentalConfig, Fundamentals, private_signal, public_observe
17+from ..exchange.accounts import Account
18+from ..exchange.engine import AssetSpec, Exchange, ExchangeConfig
19+from ..exchange.orders import Action, OrderType, Side
20+from ..signals.news import NewsConfig, NewsFeed
21+from ..utils.hashing import git_revision, stable_json
22+from ..utils.rng import agent_rng, make_streams
23+from .config import RunConfig
24+from .population import build_population
25+
26+
27+@dataclass
28+class RunResult:
29+ config: RunConfig
30+ ticks: pd.DataFrame
31+ trades: pd.DataFrame
32+ agents: pd.DataFrame
33+ llm: pd.DataFrame
34+ events: pd.DataFrame | None
35+ manifest: dict
36+ specs: list[AgentSpec] = field(default_factory=list)
37+
38+ def save(self, out_dir: str | Path) -> Path:
39+ out = Path(out_dir)
40+ out.mkdir(parents=True, exist_ok=True)
41+ self.config.to_yaml(out / "config.yaml")
42+ self.ticks.to_parquet(out / "ticks.parquet", index=False)
43+ self.trades.to_parquet(out / "trades.parquet", index=False)
44+ self.agents.to_parquet(out / "agents.parquet", index=False)
45+ self.llm.to_parquet(out / "llm_decisions.parquet", index=False)
46+ if self.events is not None:
47+ self.events.to_parquet(out / "events.parquet", index=False)
48+ with open(out / "manifest.json", "w") as f:
49+ json.dump(self.manifest, f, indent=2, default=str)
50+ return out
51+
52+
53+class Simulation:
54+ def __init__(self, cfg: RunConfig, llm_policy=None, progress: bool = False) -> None:
55+ self.cfg = cfg
56+ self.streams = make_streams(cfg.seed)
57+ self.progress = progress
58+ K = cfg.assets.n_assets
59+ fc = cfg.fundamentals
60+ self.fund = Fundamentals(FundamentalConfig(
61+ n_assets=K, div_interval=fc.div_interval, r_period=fc.r_period, mu=tuple(fc.mu), mu_shift=fc.mu_shift,
62+ p_switch=fc.p_switch, rho=fc.rho, sigma=fc.sigma, jump_prob=fc.jump_prob, jump_sigma=fc.jump_sigma,
63+ factor_loading=tuple(fc.factor_loading), x0=fc.x0), self.streams["fundamentals"])
64+ self.news = NewsFeed(NewsConfig(rate=cfg.news.rate, noise_sd=cfg.news.noise_sd, misleading_share=cfg.news.misleading_share,
65+ misleading_bias=cfg.news.misleading_bias), self.streams["news"], K)
66+ ex = cfg.exchange
67+ self.exchange = Exchange(
68+ [AssetSpec(asset=a, name=f"A{a}", tick_cents=cfg.assets.tick_cents, price_band_pct=cfg.assets.price_band_pct,
69+ max_order_qty=cfg.assets.max_order_qty) for a in range(K)],
70+ ExchangeConfig(maker_fee_bps=ex.maker_fee_bps, taker_fee_bps=ex.taker_fee_bps, min_latency=ex.min_latency,
71+ halt_threshold=ex.halt_threshold, halt_window=ex.halt_window, halt_duration=ex.halt_duration,
72+ halt_cancel_resting=ex.halt_cancel_resting, allow_short=ex.allow_short, max_short_lots=ex.max_short_lots,
73+ margin_check=ex.margin_check, liquidation_fraction=ex.liquidation_fraction, order_ttl=ex.order_ttl,
74+ position_limit=ex.position_limit, call_auction_interval=ex.call_auction_interval),
75+ self.streams["tiebreak"], {a: cfg.assets.initial_price_cents // cfg.assets.tick_cents for a in range(K)})
76+ self.specs = build_population(cfg, self.streams)
77+ self.policies: dict[int, object] = {}
78+ self.rngs: dict[int, np.random.Generator] = {}
79+ for s in self.specs:
80+ acct = Account(agent_id=s.agent_id, cash=s.cash, positions=dict(s.positions), leverage_cap=s.leverage_cap,
81+ short_allowed=s.short_allowed, maintenance_margin=cfg.population.maintenance_margin)
82+ self.exchange.register(acct, s.latency)
83+ self.rngs[s.agent_id] = agent_rng(cfg.seed, s.agent_id)
84+ if s.policy_name != "llm":
85+ self.policies[s.agent_id] = CLASSICAL_POLICIES[s.policy_name](**s.policy_kwargs)
86+ self.llm_specs = [s for s in self.specs if s.policy_name == "llm"]
87+ self.llm_policy = llm_policy
88+ if self.llm_specs and self.llm_policy is None:
89+ raise ValueError("population has LLM slots but no llm_policy was supplied")
90+ self.memory: dict[int, deque[str]] = {s.agent_id: deque(maxlen=max(cfg.population.llm.memory, 1)) for s in self.llm_specs}
91+ self.last_equity: dict[int, float] = {}
92+ self.pnl_hist: dict[int, list[float]] = {s.agent_id: [] for s in self.llm_specs}
93+ self.channel: dict[int, deque[str]] = {}
94+ self.mids = np.zeros((K, cfg.n_ticks + 1))
95+ self.rows: list[dict] = []
96+ self.shock_log: list[dict] = []
97+ self.mm_withdrawn = False
98+ self.n_llm_orders = 0
99+ self.n_llm_volume = 0
100+
101+ # ---- observation -----------------------------------------------------------------------
102+ def _observe(self, spec: AgentSpec, t: int, trades_window: dict[int, list]) -> Observation:
103+ ex, cfg = self.exchange, self.cfg
104+ acct = ex.ledger[spec.agent_id]
105+ books = {}
106+ for a, b in ex.books.items():
107+ bids, asks = b.depth(5)
108+ books[a] = BookView(best_bid=b.best_bid(), best_ask=b.best_ask(), bids=bids, asks=asks, mid=ex.mid_ticks(a),
109+ last_price=ex.state[a].last_price, halted=ex.is_halted(a))
110+ lb = cfg.output.lookback
111+ hist = {a: self.mids[a, max(0, t - lb):t + 1].copy() if t > 0 else np.array([float(ex.mid_ticks(a))]) for a in ex.books}
112+ sig, prec = {}, {}
113+ if spec.signal_precision > 0:
114+ for a in ex.books:
115+ sig[a] = private_signal(self.fund, self.rngs[spec.agent_id], spec.signal_precision, a)
116+ prec[a] = spec.signal_precision
117+ open_orders = [OwnOrderView(o.order_id, a, int(o.side), o.qty, o.remaining, o.price, t - o.submit_time)
118+ for a, b in ex.books.items() for o in b.resting_for(spec.agent_id)]
119+ for oid in ex.pending_by_agent.get(spec.agent_id, ()): # in-flight orders are known to their owner
120+ o = ex.orders[oid]
121+ open_orders.append(OwnOrderView(o.order_id, o.asset, int(o.side), o.qty, o.remaining, o.price, t - o.submit_time))
122+ marks = ex.marks()
123+ pv = self.fund.public_value()
124+ return Observation(
125+ t=t, agent_id=spec.agent_id, books=books, mid_history=hist, recent_trades=trades_window,
126+ last_dividend={a: float(self.fund.last_dividend[a]) for a in ex.books},
127+ public_value={a: float(pv[a]) for a in ex.books}, x_hat_public={a: float(self.fund.state.x_hat[a]) for a in ex.books},
128+ news=self.news.recent(t, cfg.news.window),
129+ private_signals=sig, signal_precision=prec, cash=acct.cash, free_cash=acct.free_cash,
130+ positions=dict(acct.positions), equity=acct.equity(marks), open_orders=open_orders,
131+ leverage_cap=acct.leverage_cap, short_allowed=acct.short_allowed,
132+ max_order_qty=min(cfg.assets.max_order_qty, cfg.population.llm.max_order_qty if spec.policy_name == "llm" else cfg.assets.max_order_qty),
133+ tick_cents={a: cfg.assets.tick_cents for a in ex.books}, div_interval=cfg.fundamentals.div_interval,
134+ ticks_to_dividend=cfg.fundamentals.div_interval - (t % cfg.fundamentals.div_interval), r_period=cfg.fundamentals.r_period,
135+ memory=list(self.memory.get(spec.agent_id, [])) if cfg.population.llm.memory > 0 else [],
136+ channel=list(self.channel.get(spec.cohort, [])) if (cfg.population.llm.channel and spec.cohort >= 0) else [],
137+ realized_pnl_history=self.pnl_hist.get(spec.agent_id, []))
138+
139+ # ---- shocks ----------------------------------------------------------------------------
140+ def _apply_shocks(self, t: int) -> None:
141+ for s in self.cfg.shocks:
142+ if s.time != t:
143+ continue
144+ ex = self.exchange
145+ rec = {"t": t, "shock": s.kind, "size": s.size, "asset": s.asset}
146+ if s.kind == "fundamental_jump":
147+ a = s.asset or 0
148+ self.fund.state.x[a] += s.size # latent shock, revealed only through dividends/news
149+ elif s.kind == "rate":
150+ self.fund.cfg.r_period = max(1e-4, self.fund.cfg.r_period + s.size)
151+ elif s.kind == "funding":
152+ for acct in ex.ledger.accounts.values():
153+ if acct.leverage_cap > 1.0:
154+ acct.leverage_cap = max(1.0, acct.leverage_cap * (1.0 - s.size))
155+ elif s.kind == "liquidity":
156+ for pol in self.policies.values():
157+ if getattr(pol, "name", "") == "market_maker":
158+ pol.withdrawn = True
159+ self.mm_withdrawn = True
160+ elif s.kind == "liquidity_end":
161+ for pol in self.policies.values():
162+ if getattr(pol, "name", "") == "market_maker":
163+ pol.withdrawn = False
164+ self.mm_withdrawn = False
165+ elif s.kind == "news_burst":
166+ a = s.asset or 0
167+ from ..signals.news import NewsItem
168+ item = NewsItem(time=t, asset=a, value=s.size, credible=True, noise_sd=0.5,
169+ text=f"Breaking: next dividend expected {abs(s.size):.1f} cents {'above' if s.size > 0 else 'below'} its long-run mean.")
170+ self.news.items.append(item)
171+ public_observe(self.fund, a, item.value, item.noise_sd)
172+ elif s.kind == "placebo":
173+ pass
174+ self.shock_log.append(rec)
175+ self.exchange._log("shock", **rec)
176+
177+ # ---- main loop ---------------------------------------------------------------------------
178+ def run(self) -> RunResult:
179+ cfg, ex = self.cfg, self.exchange
180+ t_start = time.perf_counter()
181+ K = cfg.assets.n_assets
182+ act_rng = self.streams["activation"]
183+ trades_seen = 0
184+ pending_liq: list[tuple[int, Action]] = []
185+ for a in range(K):
186+ self.mids[a, 0] = ex.mid_ticks(a)
187+ for t in range(cfg.n_ticks):
188+ ex.time = float(t)
189+ if t > 0 and t % cfg.fundamentals.div_interval == 0:
190+ d = self.fund.step()
191+ for a in range(K):
192+ ex.pay_dividends(a, int(round(d[a])))
193+ self._apply_shocks(t) # after the dividend: a fundamental jump stays latent until news or the next dividend
194+ for item in self.news.step(t, self.fund.state.x):
195+ public_observe(self.fund, item.asset, item.value, item.noise_sd)
196+ # liquidation orders from the previous tick's margin check
197+ for aid, act in pending_liq:
198+ ex.submit(aid, act, float(t))
199+ pending_liq = []
200+ # who acts this tick
201+ u = act_rng.random(len(self.specs))
202+ active_classical = [s for s, ui in zip(self.specs, u) if s.policy_name != "llm" and ui < s.activity]
203+ active_llm = [s for s in self.llm_specs if (t - s.decision_offset) % s.decision_interval == 0 and t >= cfg.warmup // 2]
204+ if cfg.population.llm.decision_cap is not None:
205+ active_llm = [s for s in active_llm if self._decisions_made(s.agent_id) < cfg.population.llm.decision_cap]
206+ tw = {a: [(tr.time, tr.price, tr.qty, int(tr.aggressor)) for tr in ex.trades[-30:] if tr.asset == a] for a in range(K)}
207+ order = list(active_classical)
208+ self.streams["shuffle"].shuffle(order) # type: ignore[arg-type] # separate stream: activations stay matched across arms
209+ for s in order:
210+ obs = self._observe(s, t, tw)
211+ action = self.policies[s.agent_id].decide(obs, self.rngs[s.agent_id])
212+ if not action.is_hold:
213+ ex.submit(s.agent_id, action, float(t))
214+ if active_llm:
215+ items = []
216+ for s in active_llm:
217+ obs = self._observe(s, t, tw)
218+ eq = obs.equity
219+ if s.agent_id in self.last_equity and self.last_equity[s.agent_id] > 0:
220+ self.pnl_hist[s.agent_id].append(100.0 * (eq / self.last_equity[s.agent_id] - 1.0))
221+ self.last_equity[s.agent_id] = eq
222+ items.append((obs, s.provider, s.model, s.prompt_id))
223+ results = self.llm_policy.decide_batch(items)
224+ for s, (obs, _, _, _), (action, rec) in zip(active_llm, items, results):
225+ if not action.is_hold:
226+ ex.submit(s.agent_id, action, float(t))
227+ self.n_llm_orders += len(action.orders)
228+ self._remember(s, obs, rec)
229+ ex.process_until(float(t + 1))
230+ liq = ex.margin_check()
231+ pending_liq = list(liq) # type: ignore[arg-type]
232+ ex.end_of_tick(t)
233+ for a in range(K):
234+ self.mids[a, t + 1] = ex.mid_ticks(a)
235+ self._record_tick(t, trades_seen)
236+ trades_seen = len(ex.trades)
237+ if self.progress and t % 100 == 0:
238+ print(f" t={t} trades={len(ex.trades)} mid={ex.mid_ticks(0):.1f} V={self.fund.full_value()[0]:.1f}", flush=True)
239+ return self._finish(time.perf_counter() - t_start)
240+
241+ def _decisions_made(self, agent_id: int) -> int:
242+ return sum(1 for r in self.llm_policy.records if r.agent_id == agent_id)
243+
244+ def _remember(self, s: AgentSpec, obs: Observation, rec) -> None:
245+ cfg = self.cfg.population.llm
246+ if cfg.memory > 0:
247+ msg = f"tick {obs.t}: {rec.action}"
248+ if rec.action == "place":
249+ msg += f" {rec.side} {rec.quantity} {rec.order_type}" + (f" @ {rec.limit_price:.2f}" if rec.order_type == "limit" else "")
250+ if not rec.valid:
251+ msg += f" (REJECTED: {rec.reason})"
252+ msg += f"; mid was {obs.books[0].mid * self.cfg.assets.tick_cents / 100:.2f}, equity {obs.equity / 100:.2f}"
253+ self.memory[s.agent_id].append(msg)
254+ if cfg.channel and s.cohort >= 0 and rec.rationale:
255+ ch = self.channel.setdefault(s.cohort, deque(maxlen=cfg.cohort_size))
256+ ch.append(f"trader {s.agent_id} (tick {obs.t}, {rec.action}{' ' + rec.side if rec.side else ''}): {rec.rationale}")
257+
258+ def _record_tick(self, t: int, trades_seen: int) -> None:
259+ ex = self.exchange
260+ new_trades = ex.trades[trades_seen:]
261+ fv, pv = self.fund.full_value(), self.fund.public_value()
262+ marks = ex.marks()
263+ for a, b in ex.books.items():
264+ bids, asks = b.depth(5)
265+ tr = [x for x in new_trades if x.asset == a]
266+ vol = sum(x.qty for x in tr)
267+ llm_ids = {s.agent_id for s in self.llm_specs}
268+ llm_vol = sum(x.qty for x in tr if x.buyer_id in llm_ids or x.seller_id in llm_ids)
269+ tc = self.cfg.assets.tick_cents
270+ self.rows.append({
271+ "t": t, "asset": a, "mid": ex.mid_ticks(a) * tc, "best_bid": (b.best_bid() or np.nan) * tc,
272+ "best_ask": (b.best_ask() or np.nan) * tc, "spread": (ex.spread_ticks(a) if ex.spread_ticks(a) is not None else np.nan) * tc,
273+ "depth_bid5": sum(q for _, q in bids), "depth_ask5": sum(q for _, q in asks),
274+ "last_price": (ex.state[a].last_price or np.nan) * tc, "volume": vol, "llm_volume": llm_vol,
275+ "signed_volume": sum(int(x.aggressor) * x.qty for x in tr), "n_trades": len(tr),
276+ "full_value": float(fv[a]), "public_value": float(pv[a]), "x": float(self.fund.state.x[a]),
277+ "regime": int(self.fund.state.regime[a]), "halted": ex.is_halted(a), "n_halts": ex.state[a].n_halts,
278+ "eff_spread": float(np.mean([2 * abs(x.price * tc - self._mid_before(x)) for x in tr])) if tr else np.nan,
279+ "n_resting": len(b),
280+ "total_equity": sum(acc.equity(marks) for acc in ex.ledger.accounts.values()),
281+ })
282+
283+ def _mid_before(self, trade) -> float:
284+ # mid at the start of the trade's tick (proxy for the pre-trade quote midpoint)
285+ a, t = trade.asset, int(trade.time)
286+ return self.mids[a, t] * self.cfg.assets.tick_cents
287+
288+ def _finish(self, wall: float) -> RunResult:
289+ cfg, ex = self.cfg, self.exchange
290+ ticks = pd.DataFrame(self.rows)
291+ tc = cfg.assets.tick_cents
292+ trades = pd.DataFrame([{"trade_id": x.trade_id, "time": x.time, "asset": x.asset, "price": x.price * tc, "qty": x.qty,
293+ "buyer": x.buyer_id, "seller": x.seller_id, "aggressor": int(x.aggressor),
294+ "maker_fee": x.maker_fee, "taker_fee": x.taker_fee} for x in ex.trades])
295+ marks = ex.marks()
296+ rows = []
297+ for s in self.specs:
298+ acc = ex.ledger[s.agent_id]
299+ w0 = acc.initial_cash + sum(q * cfg.assets.initial_price_cents for q in acc.initial_positions.values())
300+ w1 = acc.equity(marks)
301+ rows.append({"agent_id": s.agent_id, "policy": s.policy_name, "replaced": s.policy_kwargs.get("replaced", ""),
302+ "provider": s.provider, "model": s.model, "prompt_id": s.prompt_id, "cohort": s.cohort,
303+ "stratum": s.stratum, "latency": s.latency, "precision": s.signal_precision, "leverage_cap": s.leverage_cap,
304+ "initial_wealth": w0, "final_wealth": w1, "pnl_pct": 100.0 * (w1 / w0 - 1.0) if w0 else np.nan,
305+ "cash": acc.cash, "position": acc.positions.get(0, 0), "fees": acc.realized_fees,
306+ "dividends": acc.dividends_received, "n_orders": acc.n_orders, "n_fills": acc.n_fills,
307+ "n_rejects": acc.n_rejects, "n_cancels": acc.n_cancels, "traded_qty": acc.traded_qty, "frozen": acc.frozen})
308+ agents = pd.DataFrame(rows)
309+ if self.llm_policy is not None and self.llm_policy.records:
310+ from dataclasses import asdict
311+ llm = pd.DataFrame([asdict(r) for r in self.llm_policy.records])
312+ usage = self.llm_policy.budget.usage.as_dict()
313+ else:
314+ llm = pd.DataFrame(columns=["t", "agent_id", "provider", "model", "valid"])
315+ usage = {}
316+ events = None
317+ if cfg.output.events:
318+ events = pd.DataFrame([{"seq": e.seq, "time": e.time, "kind": e.kind, "payload": json.dumps(e.payload)} for e in ex.events])
319+ n_llm = len(self.llm_specs)
320+ n_orders = int(agents["n_orders"].sum())
321+ tot_vol = int(trades["qty"].sum()) if len(trades) else 0
322+ llm_vol = int(ticks["llm_volume"].sum()) if len(ticks) else 0
323+ manifest = {
324+ "name": cfg.name, "seed": cfg.seed, "config_hash": cfg.config_hash(), "code_version": __version__,
325+ "git_revision": git_revision(Path(__file__).resolve().parents[3]), "n_ticks": cfg.n_ticks, "n_traders": cfg.population.n_traders,
326+ "llm_share_traders": n_llm / cfg.population.n_traders, "llm_share_orders": self.n_llm_orders / max(n_orders, 1),
327+ "llm_share_volume": llm_vol / max(tot_vol, 1), "n_llm_agents": n_llm, "n_llm_decisions": int(len(llm)),
328+ "llm_valid_rate": float(llm["valid"].mean()) if len(llm) else np.nan, "llm_usage": usage,
329+ "providers": sorted({s.provider for s in self.llm_specs}), "models": sorted({s.model for s in self.llm_specs}),
330+ "prompt_version": cfg.population.llm.prompt_version, "price_assumptions": cfg.population.llm.price_assumptions,
331+ "treatment": cfg.treatment, "shocks": self.shock_log, "n_trades": len(trades), "wall_s": wall,
332+ "final_conservation_ok": True, "n_halts": int(sum(st.n_halts for st in ex.state.values())),
333+ "n_bankrupt": int(agents["frozen"].sum()), "streams": "SeedSequence(seed).spawn: " + ",".join(self.streams),
334+ }
335+ ex.ledger.check_conservation()
336+ return RunResult(config=cfg, ticks=ticks, trades=trades, agents=agents, llm=llm, events=events, manifest=manifest, specs=self.specs)
337+
338+
339+def make_llm_policy(cfg: RunConfig, cache_path: str | None = None, mock_kwargs: dict | None = None, frozen_only: bool = False):
340+ from ..llm.policy import LLMPolicy, LLMPolicyConfig
341+ l = cfg.population.llm
342+ return LLMPolicy(LLMPolicyConfig(
343+ models=l.models, prompt_version=l.prompt_version, reasoning_effort=l.reasoning_effort, temperature=l.temperature,
344+ max_output_tokens=l.max_output_tokens, concurrency=l.concurrency, timeout_s=l.timeout_s, max_retries=l.max_retries,
345+ usd_cap=l.usd_cap, token_cap=l.token_cap, cache_path=cache_path, use_cache=l.cache, frozen_only=frozen_only,
346+ adaptive_prompt=l.adaptive_prompt, mu_cents=cfg.fundamentals.mu[0], taker_bps=cfg.exchange.taker_fee_bps,
347+ order_ttl=cfg.exchange.order_ttl, price_assumptions=l.price_assumptions, library_path=l.frozen_library,
348+ mock_kwargs={"seed": cfg.seed, **(mock_kwargs or {})}))
349+
350+
351+def run_config(cfg: RunConfig, out_dir: str | Path | None = None, cache_path: str | None = None, progress: bool = False,
352+ mock_kwargs: dict | None = None) -> RunResult:
353+ llm_policy = make_llm_policy(cfg, cache_path, mock_kwargs) if cfg.population.llm_share > 0 else None
354+ res = Simulation(cfg, llm_policy=llm_policy, progress=progress).run()
355+ if out_dir is not None:
356+ res.save(out_dir)
357+ return res
added src/market_agents/llm/__init__.py +0 −0
added src/market_agents/llm/budget.py +75 −0
@@ -0,0 +1,75 @@
1+"""Token and USD accounting with a hard cap. One tracker per run (and a global one per process)."""
2+from __future__ import annotations
3+
4+import threading
5+from dataclasses import dataclass, field
6+
7+
8+class BudgetExceeded(RuntimeError):
9+ pass
10+
11+
12+@dataclass
13+class Usage:
14+ calls: int = 0
15+ cached_hits: int = 0
16+ input_tokens: int = 0
17+ cached_input_tokens: int = 0
18+ output_tokens: int = 0
19+ reasoning_tokens: int = 0
20+ usd: float = 0.0
21+ failures: int = 0
22+ invalid: int = 0
23+ latency_s: float = 0.0
24+ by_model: dict[str, dict[str, float]] = field(default_factory=dict)
25+
26+ def as_dict(self) -> dict:
27+ d = {k: getattr(self, k) for k in ("calls", "cached_hits", "input_tokens", "cached_input_tokens",
28+ "output_tokens", "reasoning_tokens", "usd", "failures", "invalid", "latency_s")}
29+ d["by_model"] = self.by_model
30+ return d
31+
32+
33+class BudgetTracker:
34+ def __init__(self, usd_cap: float, token_cap: int | None, prices: dict[str, dict[str, float]]) -> None:
35+ self.usd_cap = usd_cap
36+ self.token_cap = token_cap
37+ self.prices = prices
38+ self.usage = Usage()
39+ self._lock = threading.Lock()
40+
41+ def price(self, model: str, input_tokens: int, cached_tokens: int, output_tokens: int) -> float:
42+ p = self.prices.get(model)
43+ if p is None:
44+ # unknown model: use the most expensive known price so the cap is conservative
45+ p = max(self.prices.values(), key=lambda d: d["output"]) if self.prices else {"input": 5, "cached": 0.5, "output": 25}
46+ uncached = max(input_tokens - cached_tokens, 0)
47+ return (uncached * p["input"] + cached_tokens * p["cached"] + output_tokens * p["output"]) / 1e6
48+
49+ def check(self) -> None:
50+ if self.usage.usd >= self.usd_cap:
51+ raise BudgetExceeded(f"USD cap reached: {self.usage.usd:.4f} >= {self.usd_cap}")
52+ if self.token_cap is not None and self.usage.input_tokens + self.usage.output_tokens >= self.token_cap:
53+ raise BudgetExceeded("token cap reached")
54+
55+ def record(self, model: str, input_tokens: int, cached_tokens: int, output_tokens: int,
56+ reasoning_tokens: int = 0, latency_s: float = 0.0, cached_hit: bool = False) -> float:
57+ with self._lock:
58+ u = self.usage
59+ u.calls += 1
60+ if cached_hit:
61+ u.cached_hits += 1
62+ return 0.0
63+ cost = self.price(model, input_tokens, cached_tokens, output_tokens)
64+ u.input_tokens += input_tokens
65+ u.cached_input_tokens += cached_tokens
66+ u.output_tokens += output_tokens
67+ u.reasoning_tokens += reasoning_tokens
68+ u.usd += cost
69+ u.latency_s += latency_s
70+ m = u.by_model.setdefault(model, {"calls": 0, "input_tokens": 0, "output_tokens": 0, "usd": 0.0})
71+ m["calls"] += 1
72+ m["input_tokens"] += input_tokens
73+ m["output_tokens"] += output_tokens
74+ m["usd"] += cost
75+ return cost
added src/market_agents/llm/cache.py +51 −0
@@ -0,0 +1,51 @@
1+"""SQLite cache of raw provider responses keyed by (provider, model, prompt_version, sha256(system+user)).
2+
3+A cache hit replays the *recorded* text; it is counted separately (`cached_hits`) so a run can report
4+how many decisions were genuinely online. The same store serves as the frozen decision library."""
5+from __future__ import annotations
6+
7+import json
8+import sqlite3
9+import threading
10+import time
11+from pathlib import Path
12+
13+
14+class DecisionCache:
15+ def __init__(self, path: str | Path) -> None:
16+ self.path = Path(path)
17+ self.path.parent.mkdir(parents=True, exist_ok=True)
18+ self._lock = threading.Lock()
19+ self._conn = sqlite3.connect(str(self.path), check_same_thread=False, timeout=60.0)
20+ self._conn.execute("""CREATE TABLE IF NOT EXISTS decisions (
21+ key TEXT PRIMARY KEY, provider TEXT, model TEXT, prompt_version TEXT, prompt_id TEXT,
22+ system_prompt TEXT, user_prompt TEXT, response_text TEXT, usage_json TEXT, created REAL)""")
23+ self._conn.commit()
24+
25+ @staticmethod
26+ def key(provider: str, model: str, prompt_version: str, system_prompt: str, user_prompt: str) -> str:
27+ import hashlib
28+ h = hashlib.sha256((provider + "|" + model + "|" + prompt_version + "|" + system_prompt + "|" + user_prompt).encode()).hexdigest()
29+ return h
30+
31+ def get(self, key: str) -> tuple[str, dict] | None:
32+ with self._lock:
33+ row = self._conn.execute("SELECT response_text, usage_json FROM decisions WHERE key=?", (key,)).fetchone()
34+ if row is None:
35+ return None
36+ return row[0], json.loads(row[1] or "{}")
37+
38+ def put(self, key: str, provider: str, model: str, prompt_version: str, prompt_id: str,
39+ system_prompt: str, user_prompt: str, response_text: str, usage: dict) -> None:
40+ with self._lock:
41+ self._conn.execute("INSERT OR REPLACE INTO decisions VALUES (?,?,?,?,?,?,?,?,?,?)",
42+ (key, provider, model, prompt_version, prompt_id, system_prompt, user_prompt,
43+ response_text, json.dumps(usage), time.time()))
44+ self._conn.commit()
45+
46+ def count(self) -> int:
47+ with self._lock:
48+ return int(self._conn.execute("SELECT COUNT(*) FROM decisions").fetchone()[0])
49+
50+ def close(self) -> None:
51+ self._conn.close()
added src/market_agents/llm/library.py +96 −0
@@ -0,0 +1,96 @@
1+"""Frozen policy library: a nearest-neighbour replay of *recorded* LLM decisions.
2+
3+Every online LLM decision is logged with a feature vector of its observation; the library maps a new
4+observation to the recorded decision of the nearest neighbour in feature space (standardized Euclidean
5+distance) and rescales the quantity to the current account. This is NOT an adaptive agent: it is a
6+frozen, non-reasoning approximation used only (i) to scale populations to 1,000+ traders at zero API
7+cost and (ii) as the 'cached' arm of the cached-vs-online comparison. Runs using it are labelled
8+provider = 'library'."""
9+from __future__ import annotations
10+
11+import json
12+from pathlib import Path
13+
14+import numpy as np
15+import pandas as pd
16+
17+from ..agents.base import Observation
18+
19+FEATURES = ["mispricing", "momentum", "signal", "inv_ratio", "cash_ratio", "spread_bp", "has_open"]
20+
21+
22+def features(obs: Observation, asset: int = 0) -> np.ndarray:
23+ b = obs.books[asset]
24+ tc = obs.tick_cents[asset]
25+ mid = max(b.mid, 1.0)
26+ h = obs.mid_history[asset]
27+ mom = (h[-1] / h[0] - 1.0) if len(h) > 1 and h[0] > 0 else 0.0
28+ pv = obs.public_value[asset] / tc
29+ sig = obs.private_signals.get(asset, 0.0)
30+ pos_val = obs.positions.get(asset, 0) * mid * tc
31+ eq = max(obs.equity, 1.0)
32+ spread = ((b.best_ask - b.best_bid) / mid * 1e4) if (b.best_ask is not None and b.best_bid is not None) else 50.0
33+ return np.array([(pv - mid) / mid, mom, sig / 10.0, pos_val / eq, obs.free_cash / eq, spread / 10.0, float(bool(obs.open_orders))])
34+
35+
36+class DecisionLibrary:
37+ def __init__(self, X: np.ndarray, decisions: list[dict]) -> None:
38+ self.X = X
39+ self.mu = X.mean(axis=0)
40+ self.sd = X.std(axis=0) + 1e-9
41+ self.Z = (X - self.mu) / self.sd
42+ self.decisions = decisions
43+
44+ @classmethod
45+ def from_runs(cls, run_dirs: list[str | Path]) -> "DecisionLibrary":
46+ X, D = [], []
47+ for rd in run_dirs:
48+ p = Path(rd) / "llm_decisions.parquet"
49+ if not p.exists():
50+ continue
51+ df = pd.read_parquet(p)
52+ df = df[df["valid"] & df["features"].notna()] if "features" in df else df.iloc[0:0]
53+ for _, r in df.iterrows():
54+ X.append(json.loads(r["features"]))
55+ D.append({"action": r["action"], "side": r["side"], "order_type": r["order_type"], "quantity": int(r["quantity"]),
56+ "rel_price": float(r["rel_price"]) if "rel_price" in r and pd.notna(r["rel_price"]) else 0.0,
57+ "rationale": "library replay"})
58+ if not X:
59+ raise ValueError("no recorded decisions with features in the given runs")
60+ return cls(np.array(X, dtype=float), D)
61+
62+ def save(self, path: str | Path) -> None:
63+ np.savez(path, X=self.X, decisions=json.dumps(self.decisions))
64+
65+ @classmethod
66+ def load(cls, path: str | Path) -> "DecisionLibrary":
67+ z = np.load(path, allow_pickle=False)
68+ return cls(z["X"], json.loads(str(z["decisions"])))
69+
70+ def lookup(self, f: np.ndarray, rng: np.random.Generator, k: int = 5) -> dict:
71+ z = (f - self.mu) / self.sd
72+ d = ((self.Z - z) ** 2).sum(axis=1)
73+ idx = np.argpartition(d, min(k, len(d) - 1))[:k]
74+ return dict(self.decisions[int(rng.choice(idx))])
75+
76+ def __len__(self) -> int:
77+ return len(self.decisions)
78+
79+
80+class LibraryProvider:
81+ """Provider-shaped wrapper so the LLMPolicy pipeline (validation, logging) is reused unchanged."""
82+ name = "library"
83+
84+ def __init__(self, library_path: str, seed: int = 0, **_: object) -> None:
85+ self.lib = DecisionLibrary.load(library_path)
86+ self.rng = np.random.Generator(np.random.PCG64(seed))
87+
88+ def complete_obs(self, obs: Observation, mid_units: float) -> str:
89+ dec = self.lib.lookup(features(obs), self.rng)
90+ if dec["action"] == "place" and dec["order_type"] == "limit":
91+ dec["limit_price"] = round(mid_units * (1.0 + dec.get("rel_price", 0.0)), 2)
92+ else:
93+ dec["limit_price"] = None
94+ dec.setdefault("cancel_order_ids", [])
95+ dec.setdefault("confidence", None)
96+ return json.dumps(dec)
added src/market_agents/llm/policy.py +219 −0
@@ -0,0 +1,219 @@
1+"""LLMPolicy: Observation -> prompt -> provider -> validated Decision -> Action.
2+
3+Validation is local and strict: quantities within [1, max_order_qty], limit prices within ±20% of the
4+mid and on the tick grid, cash/holdings feasibility, cancel ids owned by the agent. Invalid decisions
5+become `hold` with meta.valid=False and a reason; they are counted in the reliability statistics.
6+Decisions are made in batches (one per decision round) with a bounded thread pool."""
7+from __future__ import annotations
8+
9+import json
10+import time
11+from concurrent.futures import ThreadPoolExecutor
12+from dataclasses import dataclass, field
13+
14+import numpy as np
15+
16+from ..agents.base import Observation
17+from ..exchange.orders import Action, CancelIntent, OrderIntent, OrderType, Side
18+from .budget import BudgetExceeded, BudgetTracker
19+from .cache import DecisionCache
20+from .prompting import PROMPT_VERSION, system_prompt, user_message
21+from .providers import ProviderResult, get_provider
22+from .schema import Decision, parse_decision
23+
24+
25+@dataclass(slots=True)
26+class LLMDecisionRecord:
27+ t: int
28+ agent_id: int
29+ provider: str
30+ model: str
31+ prompt_id: str
32+ valid: bool
33+ reason: str
34+ action: str
35+ side: str
36+ order_type: str
37+ quantity: int
38+ limit_price: float
39+ confidence: float
40+ rationale: str
41+ input_tokens: int
42+ cached_input_tokens: int
43+ output_tokens: int
44+ reasoning_tokens: int
45+ latency_s: float
46+ usd: float
47+ cached_hit: bool
48+ error: str
49+ prompt_sha: str
50+ request_id: str = ""
51+ features: str = "" # JSON feature vector (for the frozen library)
52+ rel_price: float = float("nan")
53+
54+
55+@dataclass
56+class LLMPolicyConfig:
57+ models: dict[str, str]
58+ prompt_version: str = PROMPT_VERSION
59+ reasoning_effort: str | None = "low"
60+ temperature: float | None = None
61+ max_output_tokens: int = 1200
62+ concurrency: int = 8
63+ timeout_s: float = 60.0
64+ max_retries: int = 3
65+ usd_cap: float = 5.0
66+ token_cap: int | None = None
67+ cache_path: str | None = None
68+ use_cache: bool = True
69+ frozen_only: bool = False # never call a provider; cache misses -> hold (frozen library replay)
70+ adaptive_prompt: bool = False
71+ mu_cents: float = 50.0
72+ taker_bps: float = 2.0
73+ order_ttl: int | None = 50
74+ price_assumptions: dict[str, dict[str, float]] = field(default_factory=dict)
75+ mock_kwargs: dict = field(default_factory=dict)
76+ provider_kwargs: dict = field(default_factory=dict)
77+ library_path: str | None = None
78+
79+
80+class LLMPolicy:
81+ name = "llm"
82+
83+ def __init__(self, cfg: LLMPolicyConfig) -> None:
84+ self.cfg = cfg
85+ self.budget = BudgetTracker(cfg.usd_cap, cfg.token_cap, cfg.price_assumptions)
86+ self.cache = DecisionCache(cfg.cache_path) if (cfg.use_cache and cfg.cache_path) else None
87+ self._providers: dict[str, object] = {}
88+ self.records: list[LLMDecisionRecord] = []
89+ self.halted_for_budget = False
90+
91+ def provider(self, name: str):
92+ if name not in self._providers:
93+ if name == "mock":
94+ self._providers[name] = get_provider("mock", **self.cfg.mock_kwargs)
95+ elif name == "library":
96+ from .library import LibraryProvider
97+ self._providers[name] = LibraryProvider(self.cfg.library_path, seed=self.cfg.mock_kwargs.get("seed", 0))
98+ else:
99+ self._providers[name] = get_provider(name, timeout_s=self.cfg.timeout_s, max_retries=self.cfg.max_retries,
100+ **self.cfg.provider_kwargs)
101+ return self._providers[name]
102+
103+ # ---- one decision ------------------------------------------------------------------
104+ def _call(self, provider: str, model: str, prompt_id: str, system: str, user: str) -> tuple[ProviderResult, bool]:
105+ key = DecisionCache.key(provider, model, self.cfg.prompt_version, system, user)
106+ if self.cache is not None:
107+ hit = self.cache.get(key)
108+ if hit is not None:
109+ text, usage = hit
110+ return ProviderResult(text=text, model=model, raw=usage), True
111+ if self.cfg.frozen_only or self.halted_for_budget:
112+ return ProviderResult(text="", error="frozen_miss" if self.cfg.frozen_only else "budget_halt", model=model), False
113+ try:
114+ self.budget.check()
115+ except BudgetExceeded as e:
116+ self.halted_for_budget = True
117+ return ProviderResult(text="", error=str(e), model=model), False
118+ res = self.provider(provider.split('_')[0] if provider.startswith('openai') else provider).complete(system, user, model, reasoning_effort=self.cfg.reasoning_effort,
119+ temperature=self.cfg.temperature, max_output_tokens=self.cfg.max_output_tokens,
120+ cache_key=f"ma-{self.cfg.prompt_version}-{prompt_id}")
121+ if self.cache is not None and res.text and not res.error:
122+ self.cache.put(key, provider, model, self.cfg.prompt_version, prompt_id, system, user, res.text,
123+ {"input_tokens": res.input_tokens, "output_tokens": res.output_tokens, "cached": res.cached_input_tokens})
124+ return res, False
125+
126+ def decide_one(self, obs: Observation, provider: str, model: str, prompt_id: str) -> tuple[Action, LLMDecisionRecord]:
127+ system = system_prompt(prompt_id, obs, self.cfg.mu_cents, self.cfg.taker_bps, self.cfg.order_ttl)
128+ user = user_message(obs, 0, adaptive=self.cfg.adaptive_prompt)
129+ if provider == "library":
130+ mid_units = obs.books[0].mid * obs.tick_cents[0] / 100.0
131+ res, cached_hit = ProviderResult(text=self.provider("library").complete_obs(obs, mid_units), model=model), True
132+ else:
133+ res, cached_hit = self._call(provider, model, prompt_id, system, user)
134+ cost = self.budget.record(model, res.input_tokens, res.cached_input_tokens, res.output_tokens, res.reasoning_tokens,
135+ res.latency_s, cached_hit=cached_hit)
136+ if res.error:
137+ self.budget.usage.failures += 1
138+ dec, perr = (None, res.error) if res.error else parse_decision(res.text)
139+ action, valid, reason = self.to_action(dec, obs) if dec is not None else (Action(), False, perr)
140+ if not valid:
141+ self.budget.usage.invalid += 1
142+ import hashlib
143+ rec = LLMDecisionRecord(
144+ t=obs.t, agent_id=obs.agent_id, provider=provider, model=model, prompt_id=prompt_id, valid=valid, reason=reason,
145+ action=dec.action if dec else "hold", side=(dec.side or "") if dec else "", order_type=(dec.order_type or "") if dec else "",
146+ quantity=int(dec.quantity or 0) if dec else 0, limit_price=float(dec.limit_price or 0.0) if dec else 0.0,
147+ confidence=float(dec.confidence) if (dec and dec.confidence is not None) else float("nan"),
148+ rationale=dec.rationale if dec else "", input_tokens=res.input_tokens, cached_input_tokens=res.cached_input_tokens,
149+ output_tokens=res.output_tokens, reasoning_tokens=res.reasoning_tokens, latency_s=res.latency_s, usd=cost,
150+ cached_hit=cached_hit, error=res.error, prompt_sha=hashlib.sha256((system + user).encode()).hexdigest()[:16],
151+ request_id=res.request_id)
152+ from .library import features as _feat
153+ rec.features = json.dumps([round(float(x), 6) for x in _feat(obs)])
154+ if dec is not None and dec.action == "place" and dec.order_type == "limit" and dec.limit_price:
155+ rec.rel_price = float(dec.limit_price * 100.0 / obs.tick_cents[0] / obs.books[0].mid - 1.0)
156+ action.meta.update({"valid": valid, "reason": reason, "rationale": rec.rationale, "confidence": rec.confidence,
157+ "llm_action": rec.action, "provider": provider, "model": model})
158+ self.records.append(rec)
159+ return action, rec
160+
161+ def decide_batch(self, items: list[tuple[Observation, str, str, str]]) -> list[tuple[Action, LLMDecisionRecord]]:
162+ if not items:
163+ return []
164+ if len(items) == 1 or self.cfg.concurrency <= 1:
165+ return [self.decide_one(*it) for it in items]
166+ with ThreadPoolExecutor(max_workers=self.cfg.concurrency) as ex:
167+ return list(ex.map(lambda it: self.decide_one(*it), items))
168+
169+ # ---- validation --------------------------------------------------------------------
170+ def to_action(self, dec: Decision, obs: Observation) -> tuple[Action, bool, str]:
171+ a = 0
172+ b = obs.books[a]
173+ tc = obs.tick_cents[a]
174+ act = Action()
175+ if dec.action == "hold":
176+ return act, True, ""
177+ if dec.action == "cancel":
178+ mine = {o.order_id for o in obs.open_orders}
179+ ids = [i for i in dec.cancel_order_ids if i in mine]
180+ if not ids:
181+ return act, False, "cancel: no valid order ids"
182+ act.cancels = [CancelIntent(i) for i in ids]
183+ return act, True, ""
184+ # place
185+ if dec.side is None or dec.order_type is None or dec.quantity is None:
186+ return act, False, "place: missing side/order_type/quantity"
187+ q = int(dec.quantity)
188+ if q <= 0:
189+ return act, False, "place: quantity <= 0"
190+ if q > obs.max_order_qty:
191+ return act, False, f"place: quantity {q} > max {obs.max_order_qty}"
192+ side = Side.BUY if dec.side == "buy" else Side.SELL
193+ if b.halted:
194+ return act, False, "place: market halted"
195+ if dec.order_type == "limit":
196+ if dec.limit_price is None:
197+ return act, False, "place: limit without price"
198+ px = int(round(dec.limit_price * 100.0 / tc))
199+ if px <= 0 or abs(px - b.mid) > 0.2 * b.mid:
200+ return act, False, "place: limit price outside ±20% of mid"
201+ otype = OrderType.LIMIT
202+ else:
203+ px = None
204+ otype = OrderType.MARKET
205+ if side is Side.BUY and b.best_ask is None or side is Side.SELL and b.best_bid is None:
206+ return act, False, "place: market order with empty opposite side"
207+ if side is Side.BUY:
208+ ref = px if px is not None else (b.best_ask or b.mid)
209+ need = int(ref * tc * q * 1.001)
210+ if need > obs.free_cash:
211+ return act, False, f"place: cost {need / 100:.2f} > free cash {obs.free_cash / 100:.2f}"
212+ else:
213+ held = obs.positions.get(a, 0) - sum(o.remaining for o in obs.open_orders if o.side < 0)
214+ if q > held:
215+ if held <= 0:
216+ return act, False, "place: no free shares to sell"
217+ q = held # partial validity: clip to available (logged as valid but clipped)
218+ act.orders.append(OrderIntent(a, side, otype, q, px, tag="llm"))
219+ return act, True, ""
added src/market_agents/llm/prompting.py +97 −0
@@ -0,0 +1,97 @@
1+"""Observation -> prompt. One frozen system prompt per (prompt_version, prompt_id) and a canonical,
2+deterministic user message built from the Observation. Nothing in the prompt comes from latent state."""
3+from __future__ import annotations
4+
5+from ..agents.base import Observation
6+
7+PROMPT_VERSION = "v1"
8+
9+ROLE_PERSONAS: dict[str, str] = {
10+ "generalist": "You are a professional trader managing your own account in a single-stock electronic market.",
11+ "value": "You are a value investor: you compare the price to the present value of expected dividends and trade mispricings patiently.",
12+ "momentum": "You are a momentum trader: you follow recent price trends and cut positions when the trend reverses.",
13+ "contrarian": "You are a contrarian trader: you fade sharp moves that are not justified by dividend news.",
14+ "liquidity": "You are a liquidity provider: you prefer resting limit orders near the best quotes and keep your inventory small.",
15+ "risk_averse": "You are a risk-averse trader: you keep positions small, avoid market orders and prefer to hold when uncertain.",
16+ "macro": "You are a news-driven trader: you weigh public announcements and dividend surprises and act when the market has not yet reacted.",
17+ "quant": "You are a quantitative trader: you reason about expected value per share, spread costs and inventory before each order.",
18+}
19+
20+RULES = """
21+Market mechanics (read carefully):
22+- One risky stock paying a cash dividend every {div_interval} ticks; the long-run mean dividend is {mu:.2f} per share per period and the per-period discount rate is {r_pct:.2f}%, so a share paying the mean dividend forever is worth about {pv_mean:.2f}.
23+- Dividends deviate persistently from the mean (autocorrelation {rho:.2f} per period). The deviation of the NEXT dividend is not yet announced; public news and, when you have one, your private signal are noisy estimates of it (in cents). The present value of a share rises by about 1/(1+r-rho) ≈ 44 cents per cent of next-dividend deviation.
24+- Prices are in currency units with a tick of {tick:.2f}. Limit orders rest in the book; market orders execute immediately against the best quotes (taker fee {taker_bps:.1f} bps). Orders may be partially filled. Unfilled limit orders expire after {ttl} ticks.
25+- You cannot short sell and cannot spend more than your free cash. Maximum order size: {max_qty} shares.
26+- You decide every {interval} ticks. Between decisions the market keeps trading.
27+
28+Respond with ONLY a JSON object with exactly these keys:
29+{{"action": "hold"|"place"|"cancel", "side": "buy"|"sell"|null, "order_type": "limit"|"market"|null, "quantity": integer|null, "limit_price": number|null, "cancel_order_ids": [integers], "confidence": number in [0,1]|null, "rationale": "one sentence, max 200 characters"}}
30+"place" requires side, order_type and quantity (and limit_price for a limit order). "cancel" requires cancel_order_ids. Anything else is treated as hold.
31+""".strip()
32+
33+
34+def system_prompt(prompt_id: str, obs: Observation, mu_cents: float, taker_bps: float, ttl: int | None) -> str:
35+ persona = ROLE_PERSONAS.get(prompt_id, ROLE_PERSONAS["generalist"])
36+ tick = obs.tick_cents[0] / 100.0
37+ pv_mean = (mu_cents / obs.r_period) / 100.0
38+ return persona + "\n\n" + RULES.format(
39+ div_interval=obs.div_interval, mu=mu_cents / 100.0, r_pct=obs.r_period * 100.0, pv_mean=pv_mean,
40+ rho=0.98, tick=tick, taker_bps=taker_bps, ttl=ttl if ttl is not None else 10_000,
41+ max_qty=obs.max_order_qty, interval=max(1, obs.div_interval))
42+
43+
44+def user_message(obs: Observation, asset: int = 0, adaptive: bool = False) -> str:
45+ b = obs.books[asset]
46+ tc = obs.tick_cents[asset]
47+ u = lambda ticks: f"{ticks * tc / 100.0:.2f}" # noqa: E731
48+ lines = [f"Time: tick {obs.t}. Next dividend in {obs.ticks_to_dividend} ticks."]
49+ if b.halted:
50+ lines.append("TRADING IS HALTED on this stock (orders will be rejected).")
51+ bb = u(b.best_bid) if b.best_bid is not None else "none"
52+ ba = u(b.best_ask) if b.best_ask is not None else "none"
53+ lines.append(f"Best bid {bb} / best ask {ba} / mid {u(b.mid)} / last trade {u(b.last_price) if b.last_price else 'none'}.")
54+ depth_b = ", ".join(f"{u(p)}x{q}" for p, q in b.bids[:5]) or "empty"
55+ depth_a = ", ".join(f"{u(p)}x{q}" for p, q in b.asks[:5]) or "empty"
56+ lines.append(f"Bid depth (price x shares): {depth_b}. Ask depth: {depth_a}.")
57+ h = obs.mid_history[asset]
58+ if len(h) >= 2:
59+ pts = [u(x) for x in h[::-max(1, len(h) // 10)][::-1]]
60+ lines.append(f"Mid-price history (oldest to newest, {len(h)} ticks sampled): " + ", ".join(pts) + ".")
61+ r = (h[-1] / h[0] - 1.0) * 100.0
62+ lines.append(f"Change over the window: {r:+.2f}%. Recent volatility (std of tick returns): {100 * (h[1:] / h[:-1] - 1).std():.3f}%.")
63+ tr = obs.recent_trades.get(asset, [])
64+ if tr:
65+ vol = sum(q for _, _, q, _ in tr)
66+ buyv = sum(q for _, _, q, ag in tr if ag > 0)
67+ lines.append(f"Last {len(tr)} trades: volume {vol} shares, {100 * buyv / max(vol, 1):.0f}% buyer-initiated.")
68+ lines.append(f"Last dividend paid: {obs.last_dividend[asset] / 100.0:.2f} per share. Public estimate of the next dividend's deviation "
69+ f"from its mean: public estimate {obs.x_hat_public.get(asset, 0.0):+.1f} cents. "
70+ f"Present value of expected dividends using only public information: {obs.public_value[asset] / 100.0:.2f}.")
71+ if obs.news:
72+ for n in obs.news[-3:]:
73+ lines.append(f"Public news (tick {n.time}): {n.text}")
74+ if asset in obs.private_signals:
75+ lines.append(f"Your private signal: next dividend deviation estimated at {obs.private_signals[asset]:+.1f} cents "
76+ f"(signal precision {obs.signal_precision.get(asset, 0):.1f}; higher is more reliable).")
77+ else:
78+ lines.append("You have no private signal this period.")
79+ lines.append(f"Your account: cash {obs.cash / 100.0:.2f} (free {obs.free_cash / 100.0:.2f}), "
80+ f"holdings {obs.positions.get(asset, 0)} shares, equity {obs.equity / 100.0:.2f}.")
81+ if obs.open_orders:
82+ oo = "; ".join(f"id {o.order_id}: {'buy' if o.side > 0 else 'sell'} {o.remaining}/{o.qty} @ {u(o.price) if o.price else 'mkt'} (age {o.age:.0f})"
83+ for o in obs.open_orders[:6])
84+ lines.append(f"Your open orders: {oo}.")
85+ else:
86+ lines.append("You have no open orders.")
87+ if adaptive and obs.realized_pnl_history:
88+ lines.append(f"Feedback: your equity changed by {obs.realized_pnl_history[-1]:+.2f}% since your last decision "
89+ f"(cumulative {sum(obs.realized_pnl_history):+.2f}%).")
90+ if obs.memory:
91+ lines.append("Your recent decisions and outcomes:")
92+ lines += [f" - {m}" for m in obs.memory]
93+ if obs.channel:
94+ lines.append("Messages from other traders in your group (unverified opinions):")
95+ lines += [f" - {m}" for m in obs.channel]
96+ lines.append("Decide now. Reply with the JSON object only.")
97+ return "\n".join(lines)
added src/market_agents/llm/providers/__init__.py +31 −0
@@ -0,0 +1,31 @@
1+"""Provider adapters. Each exposes `complete(system, user, model, **opts) -> ProviderResult`."""
2+from __future__ import annotations
3+
4+from dataclasses import dataclass, field
5+
6+
7+@dataclass(slots=True)
8+class ProviderResult:
9+ text: str
10+ input_tokens: int = 0
11+ cached_input_tokens: int = 0
12+ output_tokens: int = 0
13+ reasoning_tokens: int = 0
14+ latency_s: float = 0.0
15+ model: str = ""
16+ request_id: str = ""
17+ error: str = ""
18+ raw: dict = field(default_factory=dict)
19+
20+
21+def get_provider(name: str, **kwargs):
22+ if name == "mock":
23+ from .mock import MockProvider
24+ return MockProvider(**kwargs)
25+ if name in ("openai", "openai_mini", "openai_large"):
26+ from .openai_provider import OpenAIProvider
27+ return OpenAIProvider(**kwargs)
28+ if name == "anthropic":
29+ from .anthropic_provider import AnthropicProvider
30+ return AnthropicProvider(**kwargs)
31+ raise ValueError(f"unknown provider {name!r}")
added src/market_agents/llm/providers/anthropic_provider.py +55 −0
@@ -0,0 +1,55 @@
1+"""Anthropic adapter (official `anthropic` SDK, Messages API with structured output).
2+
3+Reads ANTHROPIC_API_KEY from the environment only. The frozen system prompt is marked for prompt
4+caching; the user message carries the volatile market snapshot. `effort` maps to output_config.effort
5+for models that support it (Claude 4.6+). No thinking parameter is sent (adaptive by default)."""
6+from __future__ import annotations
7+
8+import os
9+import time
10+
11+from . import ProviderResult
12+from ..schema import DECISION_JSON_SCHEMA
13+
14+
15+class AnthropicProvider:
16+ name = "anthropic"
17+
18+ def __init__(self, timeout_s: float = 60.0, max_retries: int = 3, **_: object) -> None:
19+ import anthropic
20+ if not os.environ.get("ANTHROPIC_API_KEY"):
21+ raise RuntimeError("ANTHROPIC_API_KEY is not set")
22+ self._anthropic = anthropic
23+ self.client = anthropic.Anthropic(timeout=timeout_s, max_retries=max_retries)
24+
25+ def complete(self, system: str, user: str, model: str, reasoning_effort: str | None = "low",
26+ max_output_tokens: int = 400, **_: object) -> ProviderResult:
27+ t0 = time.perf_counter()
28+ kwargs: dict = dict(
29+ model=model,
30+ max_tokens=max(max_output_tokens, 256),
31+ system=[{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}],
32+ messages=[{"role": "user", "content": user}],
33+ output_config={"format": {"type": "json_schema", "schema": DECISION_JSON_SCHEMA}},
34+ )
35+ if reasoning_effort:
36+ kwargs["output_config"]["effort"] = reasoning_effort
37+ try:
38+ resp = self.client.messages.create(**kwargs)
39+ except self._anthropic.RateLimitError as e:
40+ return ProviderResult(text="", error=f"rate_limit: {e}", latency_s=time.perf_counter() - t0, model=model)
41+ except self._anthropic.APIStatusError as e:
42+ return ProviderResult(text="", error=f"status_{e.status_code}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)
43+ except self._anthropic.APIConnectionError as e:
44+ return ProviderResult(text="", error=f"connection: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)
45+ except Exception as e: # noqa: BLE001
46+ return ProviderResult(text="", error=f"other: {type(e).__name__}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)
47+ if getattr(resp, "stop_reason", "") == "refusal":
48+ return ProviderResult(text="", error="refusal", latency_s=time.perf_counter() - t0, model=model)
49+ text = "".join(b.text for b in resp.content if getattr(b, "type", "") == "text")
50+ u = resp.usage
51+ return ProviderResult(text=text, input_tokens=int(u.input_tokens or 0),
52+ cached_input_tokens=int(getattr(u, "cache_read_input_tokens", 0) or 0),
53+ output_tokens=int(u.output_tokens or 0), latency_s=time.perf_counter() - t0,
54+ model=model, request_id=getattr(resp, "_request_id", "") or "",
55+ error="" if text else "empty_output")
added src/market_agents/llm/providers/mock.py +78 −0
@@ -0,0 +1,78 @@
1+"""Deterministic mock provider for dry runs and tests.
2+
3+It parses the canonical user message (mid, public PV, private signal, holdings) and returns a JSON
4+decision from a simple, *seeded* value-plus-noise rule — including a configurable share of malformed
5+or invalid answers to exercise the validation path. Token counts are approximated by word counts and
6+priced at zero."""
7+from __future__ import annotations
8+
9+import json
10+import re
11+import time
12+
13+import numpy as np
14+
15+from . import ProviderResult
16+
17+
18+class MockProvider:
19+ name = "mock"
20+
21+ def __init__(self, seed: int = 0, invalid_rate: float = 0.05, latency_s: float = 0.0,
22+ herd_bias: float = 0.0, **_: object) -> None:
23+ self.rng = np.random.Generator(np.random.PCG64(seed))
24+ self.invalid_rate = invalid_rate
25+ self.latency_s = latency_s
26+ self.herd_bias = herd_bias # common tilt applied to every mock agent (shared-model correlation knob)
27+ self.calls = 0
28+
29+ @staticmethod
30+ def _grab(pattern: str, text: str, default: float | None = None) -> float | None:
31+ m = re.search(pattern, text)
32+ return float(m.group(1)) if m else default
33+
34+ def complete(self, system: str, user: str, model: str, **opts) -> ProviderResult:
35+ self.calls += 1
36+ t0 = time.perf_counter()
37+ if self.latency_s:
38+ time.sleep(self.latency_s)
39+ r = self.rng.random()
40+ if r < self.invalid_rate / 2:
41+ text = "I think the market will go up so I will buy." # not JSON
42+ elif r < self.invalid_rate:
43+ text = json.dumps({"action": "place", "side": "buy", "order_type": "limit", "quantity": -5,
44+ "limit_price": None, "cancel_order_ids": [], "confidence": 2.0, "rationale": "bad"})
45+ else:
46+ mid = self._grab(r"mid ([0-9]+\.?[0-9]*)", user, None)
47+ pv = self._grab(r"public information: ([0-9]+\.?[0-9]*)", user, None)
48+ sig = self._grab(r"estimated at ([+-]?[0-9.]+) cents", user, 0.0) or 0.0
49+ xhat = self._grab(r"public estimate ([+-]?[0-9.]+)", user, 0.0) or 0.0
50+ held = int(self._grab(r"holdings (-?[0-9]+) shares", user, 0) or 0)
51+ free = self._grab(r"\(free ([0-9]+\.?[0-9]*)\)", user, 0.0) or 0.0
52+ if mid is None or pv is None:
53+ text = json.dumps({"action": "hold", "side": None, "order_type": None, "quantity": None,
54+ "limit_price": None, "cancel_order_ids": [], "confidence": 0.5, "rationale": "no data"})
55+ else:
56+ v_hat = pv + 0.5 * (sig - xhat) / (1 + 0.0025 - 0.98) / 100.0
57+ m = (v_hat - mid) / mid + self.herd_bias + 0.004 * self.rng.standard_normal()
58+ if abs(m) < 0.005:
59+ dec = {"action": "hold", "side": None, "order_type": None, "quantity": None, "limit_price": None,
60+ "cancel_order_ids": [], "confidence": 0.4, "rationale": "price close to value"}
61+ else:
62+ q = int(min(50, max(1, round(abs(m) * 1500))))
63+ if m > 0:
64+ q = min(q, int(free // (mid * 1.001))) if mid > 0 else 0
65+ dec = {"action": "place", "side": "buy", "order_type": "limit" if self.rng.random() < 0.6 else "market",
66+ "quantity": q, "limit_price": round(mid * 1.002, 2), "cancel_order_ids": [], "confidence": 0.6,
67+ "rationale": f"value {v_hat:.2f} above mid {mid:.2f}"}
68+ else:
69+ q = min(q, max(held, 0))
70+ dec = {"action": "place", "side": "sell", "order_type": "limit" if self.rng.random() < 0.6 else "market",
71+ "quantity": q, "limit_price": round(mid * 0.998, 2), "cancel_order_ids": [], "confidence": 0.6,
72+ "rationale": f"value {v_hat:.2f} below mid {mid:.2f}"}
73+ if dec["quantity"] <= 0:
74+ dec = {"action": "hold", "side": None, "order_type": None, "quantity": None, "limit_price": None,
75+ "cancel_order_ids": [], "confidence": 0.3, "rationale": "constrained"}
76+ text = json.dumps(dec)
77+ return ProviderResult(text=text, input_tokens=len(system.split()) + len(user.split()), output_tokens=len(text.split()),
78+ latency_s=time.perf_counter() - t0, model=model)
added src/market_agents/llm/providers/openai_provider.py +70 −0
@@ -0,0 +1,70 @@
1+"""OpenAI adapter (official `openai` SDK, Responses API with a JSON-schema text format).
2+
3+Reads OPENAI_API_KEY from the environment only. Model IDs come from the run config. Reasoning effort
4+is passed only for models that accept it (gpt-5 family); temperature only when the config sets it."""
5+from __future__ import annotations
6+
7+import os
8+import time
9+
10+from . import ProviderResult
11+from ..schema import DECISION_JSON_SCHEMA
12+
13+
14+class OpenAIProvider:
15+ name = "openai"
16+
17+ def __init__(self, timeout_s: float = 60.0, max_retries: int = 3, **_: object) -> None:
18+ import openai
19+ if not os.environ.get("OPENAI_API_KEY"):
20+ raise RuntimeError("OPENAI_API_KEY is not set")
21+ self._openai = openai
22+ self.client = openai.OpenAI(timeout=timeout_s, max_retries=max_retries)
23+
24+ def complete(self, system: str, user: str, model: str, reasoning_effort: str | None = "low",
25+ temperature: float | None = None, max_output_tokens: int = 400, cache_key: str | None = None,
26+ **_: object) -> ProviderResult:
27+ t0 = time.perf_counter()
28+ kwargs: dict = dict(
29+ model=model,
30+ instructions=system,
31+ input=[{"role": "user", "content": user}],
32+ max_output_tokens=max_output_tokens,
33+ text={"format": {"type": "json_schema", "name": "trading_decision", "schema": DECISION_JSON_SCHEMA, "strict": True}},
34+ store=False,
35+ )
36+ if reasoning_effort and (model.startswith("gpt-5") or model.startswith("o")):
37+ kwargs["reasoning"] = {"effort": reasoning_effort}
38+ if temperature is not None and not (model.startswith("gpt-5") or model.startswith("o")):
39+ kwargs["temperature"] = temperature
40+ if cache_key:
41+ kwargs["prompt_cache_key"] = cache_key
42+ try:
43+ resp = self.client.responses.create(**kwargs)
44+ except self._openai.RateLimitError as e:
45+ return ProviderResult(text="", error=f"rate_limit: {e}", latency_s=time.perf_counter() - t0, model=model)
46+ except self._openai.APIStatusError as e:
47+ return ProviderResult(text="", error=f"status_{e.status_code}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)
48+ except self._openai.APIConnectionError as e:
49+ return ProviderResult(text="", error=f"connection: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)
50+ except Exception as e: # noqa: BLE001
51+ return ProviderResult(text="", error=f"other: {type(e).__name__}: {str(e)[:200]}", latency_s=time.perf_counter() - t0, model=model)
52+ text = getattr(resp, "output_text", "") or ""
53+ u = getattr(resp, "usage", None)
54+ inp = int(getattr(u, "input_tokens", 0) or 0)
55+ out = int(getattr(u, "output_tokens", 0) or 0)
56+ cached = 0
57+ reasoning = 0
58+ try:
59+ cached = int(u.input_tokens_details.cached_tokens or 0)
60+ except Exception: # noqa: BLE001
61+ pass
62+ try:
63+ reasoning = int(u.output_tokens_details.reasoning_tokens or 0)
64+ except Exception: # noqa: BLE001
65+ pass
66+ status = getattr(resp, "status", "")
67+ err = "" if text else f"empty_output(status={status})"
68+ return ProviderResult(text=text, input_tokens=inp, cached_input_tokens=cached, output_tokens=out,
69+ reasoning_tokens=reasoning, latency_s=time.perf_counter() - t0, model=model,
70+ request_id=getattr(resp, "id", "") or "", error=err)
added src/market_agents/llm/schema.py +65 −0
@@ -0,0 +1,65 @@
1+"""The strictly validated decision schema returned by every LLM policy (and the mock)."""
2+from __future__ import annotations
3+
4+from typing import Literal
5+
6+from pydantic import BaseModel, Field, ValidationError, field_validator
7+
8+
9+class Decision(BaseModel):
10+ action: Literal["hold", "place", "cancel"]
11+ side: Literal["buy", "sell"] | None = None
12+ order_type: Literal["limit", "market"] | None = None
13+ quantity: int | None = Field(default=None, ge=0)
14+ limit_price: float | None = Field(default=None, gt=0) # currency units (e.g. 199.50)
15+ cancel_order_ids: list[int] = Field(default_factory=list)
16+ confidence: float | None = Field(default=None, ge=0.0, le=1.0)
17+ rationale: str = Field(default="", max_length=400)
18+
19+ @field_validator("rationale")
20+ @classmethod
21+ def _trim(cls, v: str) -> str:
22+ return v.strip()[:400]
23+
24+
25+DECISION_JSON_SCHEMA: dict = {
26+ "type": "object",
27+ "properties": {
28+ "action": {"type": "string", "enum": ["hold", "place", "cancel"]},
29+ "side": {"type": ["string", "null"], "enum": ["buy", "sell", None]},
30+ "order_type": {"type": ["string", "null"], "enum": ["limit", "market", None]},
31+ "quantity": {"type": ["integer", "null"]},
32+ "limit_price": {"type": ["number", "null"]},
33+ "cancel_order_ids": {"type": "array", "items": {"type": "integer"}},
34+ "confidence": {"type": ["number", "null"]},
35+ "rationale": {"type": "string"},
36+ },
37+ "required": ["action", "side", "order_type", "quantity", "limit_price", "cancel_order_ids", "confidence", "rationale"],
38+ "additionalProperties": False,
39+}
40+
41+
42+def parse_decision(text: str) -> tuple[Decision | None, str]:
43+ """Parse the model's JSON text; returns (decision, error). Lenient about code fences."""
44+ import json
45+ s = text.strip()
46+ if s.startswith("```"):
47+ s = s.strip("`")
48+ if s.lower().startswith("json"):
49+ s = s[4:]
50+ try:
51+ obj = json.loads(s)
52+ except json.JSONDecodeError as e:
53+ # try to locate the first {...} block
54+ i, j = s.find("{"), s.rfind("}")
55+ if i >= 0 and j > i:
56+ try:
57+ obj = json.loads(s[i:j + 1])
58+ except json.JSONDecodeError:
59+ return None, f"json: {e.msg}"
60+ else:
61+ return None, f"json: {e.msg}"
62+ try:
63+ return Decision.model_validate(obj), ""
64+ except ValidationError as e:
65+ return None, "schema: " + "; ".join(f"{'.'.join(map(str, err['loc']))}: {err['msg']}" for err in e.errors()[:3])
added src/market_agents/metrics/__init__.py +1 −0
@@ -0,0 +1 @@
1+from .outcomes import run_outcomes, series_outcomes # noqa: F401
added src/market_agents/metrics/outcomes.py +269 −0
@@ -0,0 +1,269 @@
1+"""Run-level outcome metrics. One dict per run; every number is traceable to ticks/trades/agents/llm frames.
2+
3+Primary outcomes (pre-registered in docs/identification.md):
4+ efficiency : msd_log (mean squared log deviation of mid from the latent value), mad_pct, time_to_absorb,
5+ post-shock response ratio, variance ratio VR(5) of log mid changes.
6+ liquidity : quoted spread (bp), effective spread (bp), depth (lots within 5 levels), Kyle lambda
7+ (bp per 100 lots signed volume), fill rate, resiliency (half-life of spread after a widening).
8+ instability : bubble amplitude/duration (P/V - 1 above `bubble_thr` for >= `bubble_min_len` ticks),
9+ max drawdown, crash count (drawdown >= crash_thr within crash_window), volatility clustering
10+ (sum of ACF(|r|) lags 1..10), excess kurtosis of tick returns, halts.
11+ contagion : (multi-asset) excess return correlation over fundamental correlation, Diebold-Yilmaz total
12+ spillover from a VAR(2) FEVD, lead-lag cross-correlation.
13+Secondary: volume, turnover, cancel ratio, P&L by policy, Gini, bankruptcies, MM inventory, LLM validity/cost.
14+"""
15+from __future__ import annotations
16+
17+import numpy as np
18+import pandas as pd
19+
20+
21+def _acf(x: np.ndarray, lags: int) -> np.ndarray:
22+ x = np.asarray(x, dtype=float)
23+ x = x - x.mean()
24+ n = len(x)
25+ denom = (x ** 2).sum()
26+ if denom <= 0 or n <= lags + 1:
27+ return np.full(lags, np.nan)
28+ return np.array([(x[:-k] * x[k:]).sum() / denom for k in range(1, lags + 1)])
29+
30+
31+def variance_ratio(logp: np.ndarray, q: int = 5) -> float:
32+ r = np.diff(logp)
33+ if len(r) < 5 * q or r.var() <= 0:
34+ return np.nan
35+ rq = logp[q:] - logp[:-q]
36+ return float(rq.var() / (q * r.var()))
37+
38+
39+def gini(w: np.ndarray) -> float:
40+ w = np.sort(np.clip(np.asarray(w, dtype=float), 0, None))
41+ n = len(w)
42+ if n == 0 or w.sum() <= 0:
43+ return np.nan
44+ return float((2 * np.arange(1, n + 1) - n - 1).dot(w) / (n * w.sum()))
45+
46+
47+def bubbles(ratio: np.ndarray, thr: float, min_len: int) -> tuple[int, float, float]:
48+ """Episodes where ratio = P/V - 1 stays above thr (or below -thr) for >= min_len consecutive ticks."""
49+ above = np.abs(ratio) > thr
50+ n, amp, dur = 0, 0.0, 0.0
51+ i = 0
52+ while i < len(above):
53+ if above[i]:
54+ j = i
55+ while j < len(above) and above[j]:
56+ j += 1
57+ if j - i >= min_len:
58+ n += 1
59+ amp = max(amp, float(np.abs(ratio[i:j]).max()))
60+ dur += j - i
61+ i = j
62+ else:
63+ i += 1
64+ return n, amp, dur
65+
66+
67+def drawdowns(p: np.ndarray, crash_thr: float, window: int) -> tuple[float, int]:
68+ p = np.asarray(p, dtype=float)
69+ run_max = np.maximum.accumulate(p)
70+ dd = 1.0 - p / run_max
71+ max_dd = float(dd.max()) if len(dd) else np.nan
72+ # crash: drop of >= crash_thr from the max of the trailing `window` ticks
73+ n_crash = 0
74+ i = window
75+ while i < len(p):
76+ local_max = p[i - window:i].max()
77+ if p[i] <= (1 - crash_thr) * local_max:
78+ n_crash += 1
79+ i += window # one crash per window
80+ else:
81+ i += 1
82+ return max_dd, n_crash
83+
84+
85+def kyle_lambda(ticks: pd.DataFrame) -> float:
86+ """OLS of mid change (bp) on signed volume (per 100 lots), tick by tick."""
87+ d = ticks.dropna(subset=["mid"])
88+ if len(d) < 30:
89+ return np.nan
90+ y = 1e4 * np.diff(np.log(d["mid"].to_numpy()))
91+ x = d["signed_volume"].to_numpy()[1:] / 100.0
92+ if x.var() <= 0:
93+ return np.nan
94+ return float(np.cov(x, y)[0, 1] / x.var())
95+
96+
97+def time_to_absorb(ticks: pd.DataFrame, shock_t: int, tol: float = 0.01, hold: int = 5, max_h: int = 100) -> float:
98+ """First horizon h after shock_t such that |P/V - 1| < tol for `hold` consecutive ticks."""
99+ d = ticks[(ticks.t >= shock_t) & (ticks.t < shock_t + max_h)]
100+ dev = np.abs(d["mid"].to_numpy() / d["full_value"].to_numpy() - 1.0)
101+ ok = dev < tol
102+ for h in range(len(ok) - hold + 1):
103+ if ok[h:h + hold].all():
104+ return float(h)
105+ return float(max_h)
106+
107+
108+def response_ratio(ticks: pd.DataFrame, shock_t: int, h: int = 20) -> float:
109+ """(P_{t+h} - P_{t-1}) / (V_{t+h} - V_{t-1}) : 1 = full absorption; <1 under-reaction; >1 over-reaction."""
110+ d = ticks.set_index("t")
111+ try:
112+ dp = d.loc[shock_t + h, "mid"] - d.loc[shock_t - 1, "mid"]
113+ dv = d.loc[shock_t + h, "full_value"] - d.loc[shock_t - 1, "full_value"]
114+ except KeyError:
115+ return np.nan
116+ return float(dp / dv) if abs(dv) > 1e-9 else np.nan
117+
118+
119+def resiliency(spread: np.ndarray) -> float:
120+ """Half-life (ticks) of spread deviations from their median: from AR(1) of log spread."""
121+ s = np.log(np.clip(spread[~np.isnan(spread)], 1e-6, None))
122+ if len(s) < 30:
123+ return np.nan
124+ s = s - np.median(s)
125+ phi = (s[:-1] * s[1:]).sum() / max((s[:-1] ** 2).sum(), 1e-12)
126+ phi = min(max(phi, 1e-6), 0.999)
127+ return float(np.log(0.5) / np.log(phi))
128+
129+
130+def series_outcomes(ticks: pd.DataFrame, warmup: int = 50, bubble_thr: float = 0.05, bubble_min_len: int = 10,
131+ crash_thr: float = 0.10, crash_window: int = 20, shocks: list[dict] | None = None) -> dict:
132+ out: dict = {}
133+ for a, d in ticks.groupby("asset"):
134+ d = d[d.t >= warmup].sort_values("t")
135+ P, V = d["mid"].to_numpy(), d["full_value"].to_numpy()
136+ logp = np.log(P)
137+ r = np.diff(logp)
138+ sfx = "" if a == 0 else f"_a{a}"
139+ dev = logp - np.log(V)
140+ out["msd_log" + sfx] = float(np.mean(dev ** 2))
141+ out["mad_pct" + sfx] = float(100 * np.mean(np.abs(dev)))
142+ out["bias_pct" + sfx] = float(100 * np.mean(dev))
143+ out["vr5" + sfx] = variance_ratio(logp, 5)
144+ out["vr10" + sfx] = variance_ratio(logp, 10)
145+ out["vol_bp" + sfx] = float(1e4 * r.std()) if len(r) > 2 else np.nan
146+ out["kurtosis" + sfx] = float(pd.Series(r).kurt()) if len(r) > 10 else np.nan
147+ out["acf_absr_1_10" + sfx] = float(np.nansum(_acf(np.abs(r), 10))) if len(r) > 30 else np.nan
148+ out["acf_r_1" + sfx] = float(_acf(r, 1)[0]) if len(r) > 30 else np.nan
149+ with np.errstate(all="ignore"):
150+ import warnings
151+ warnings.simplefilter("ignore", RuntimeWarning)
152+ out["spread_bp" + sfx] = float(np.nanmean(1e4 * d["spread"] / d["mid"])) if d["spread"].notna().any() else np.nan
153+ out["eff_spread_bp" + sfx] = float(1e4 * np.nanmean(d["eff_spread"] / d["mid"]))
154+ out["depth5" + sfx] = float(np.mean(d["depth_bid5"] + d["depth_ask5"]))
155+ out["lambda_bp_100" + sfx] = kyle_lambda(d)
156+ out["resiliency" + sfx] = resiliency(d["spread"].to_numpy())
157+ nb, amp, dur = bubbles(P / V - 1.0, bubble_thr, bubble_min_len)
158+ out["n_bubbles" + sfx], out["bubble_amp" + sfx], out["bubble_dur" + sfx] = nb, amp, dur
159+ out["max_dd" + sfx], out["n_crashes" + sfx] = drawdowns(P, crash_thr, crash_window)
160+ out["volume" + sfx] = float(d["volume"].sum())
161+ out["n_halts" + sfx] = int(d["n_halts"].max()) if "n_halts" in d else 0
162+ out["share_ticks_halted" + sfx] = float(d["halted"].mean()) if "halted" in d else 0.0
163+ if shocks:
164+ tta, rr = [], []
165+ for s in shocks:
166+ if s.get("shock") in ("fundamental_jump", "news_burst") and s["t"] >= warmup:
167+ tta.append(time_to_absorb(ticks[ticks.asset == a], s["t"]))
168+ rr.append(response_ratio(ticks[ticks.asset == a], s["t"]))
169+ if tta:
170+ out["time_to_absorb" + sfx] = float(np.mean(tta))
171+ out["response_ratio" + sfx] = float(np.nanmean(rr))
172+ assets = sorted(ticks["asset"].unique())
173+ if len(assets) >= 2:
174+ piv = ticks[ticks.t >= warmup].pivot(index="t", columns="asset", values="mid")
175+ fv = ticks[ticks.t >= warmup].pivot(index="t", columns="asset", values="full_value")
176+ rp, rv = np.log(piv).diff().dropna(), np.log(fv).diff().dropna()
177+ cp, cv = rp.corr().to_numpy(), rv.corr().to_numpy()
178+ iu = np.triu_indices(len(assets), 1)
179+ out["ret_corr"] = float(np.nanmean(cp[iu]))
180+ out["fund_corr"] = float(np.nanmean(cv[iu]))
181+ out["excess_comovement"] = out["ret_corr"] - out["fund_corr"]
182+ out["spillover_dy"] = diebold_yilmaz(rp.to_numpy())
183+ out["leadlag_01"] = float(np.corrcoef(rp.iloc[:-1, 0], rp.iloc[1:, 1])[0, 1]) if len(rp) > 10 else np.nan
184+ return out
185+
186+
187+def diebold_yilmaz(R: np.ndarray, p: int = 2, h: int = 10) -> float:
188+ """Total spillover index from the generalized FEVD of a VAR(p) (Diebold–Yilmaz 2012)."""
189+ T, K = R.shape
190+ if T < 10 * K * p + 10:
191+ return np.nan
192+ X = np.hstack([R[p - j - 1:T - j - 1] for j in range(p)])
193+ Y = R[p:]
194+ X = np.hstack([np.ones((len(Y), 1)), X])
195+ B, *_ = np.linalg.lstsq(X, Y, rcond=None)
196+ E = Y - X @ B
197+ Sigma = np.cov(E.T)
198+ A = [np.eye(K)]
199+ coefs = [B[1 + K * j:1 + K * (j + 1)].T for j in range(p)]
200+ for i in range(1, h):
201+ A.append(sum(coefs[j] @ A[i - j - 1] for j in range(p) if i - j - 1 >= 0))
202+ sig = np.diag(Sigma)
203+ theta = np.zeros((K, K))
204+ for i in range(K):
205+ for j in range(K):
206+ num = sum((A[l][i] @ Sigma[:, j]) ** 2 for l in range(h)) / sig[j]
207+ den = sum(A[l][i] @ Sigma @ A[l][i] for l in range(h))
208+ theta[i, j] = num / den
209+ theta = theta / theta.sum(axis=1, keepdims=True)
210+ return float(100 * (theta.sum() - np.trace(theta)) / K)
211+
212+
213+def agent_outcomes(agents: pd.DataFrame, trades: pd.DataFrame, llm: pd.DataFrame, n_ticks: int) -> dict:
214+ out: dict = {}
215+ g = agents.groupby("policy")["pnl_pct"].mean()
216+ for k, v in g.items():
217+ out[f"pnl_{k}"] = float(v)
218+ out["gini_final"] = gini(agents["final_wealth"].to_numpy())
219+ out["gini_initial"] = gini(agents["initial_wealth"].to_numpy())
220+ out["n_bankrupt"] = int(agents["frozen"].sum())
221+ out["cancel_ratio"] = float(agents["n_cancels"].sum() / max(agents["n_orders"].sum(), 1))
222+ out["reject_ratio"] = float(agents["n_rejects"].sum() / max(agents["n_orders"].sum(), 1))
223+ out["fill_ratio"] = float(agents["n_fills"].sum() / max(agents["n_orders"].sum(), 1))
224+ out["turnover"] = float(trades["qty"].sum() / max(agents["position"].abs().sum() + 1, 1)) if len(trades) else 0.0
225+ mm = agents[agents.policy == "market_maker"]
226+ if len(mm):
227+ out["mm_pnl"] = float(mm["pnl_pct"].mean())
228+ out["mm_inventory_abs"] = float(mm["position"].abs().mean())
229+ if len(llm):
230+ out["llm_valid_rate"] = float(llm["valid"].mean())
231+ out["llm_hold_rate"] = float((llm["action"] == "hold").mean())
232+ out["llm_market_share"] = float((llm["order_type"] == "market").mean())
233+ out["llm_usd"] = float(llm["usd"].sum())
234+ out["llm_tokens"] = float(llm["input_tokens"].sum() + llm["output_tokens"].sum())
235+ out["llm_latency_s"] = float(llm["latency_s"].mean())
236+ out["llm_cached_hits"] = float(llm["cached_hit"].mean())
237+ # synchrony and order-flow correlation among LLM agents: decisions grouped by decision round
238+ d = llm[llm.valid & (llm.action == "place")].copy()
239+ if len(d) > 5:
240+ d["sign"] = np.where(d["side"] == "buy", 1, -1)
241+ rounds = d.groupby("t")["sign"]
242+ sync = rounds.apply(lambda s: abs(s.mean()) if len(s) > 1 else np.nan).dropna()
243+ out["llm_synchrony"] = float(sync.mean()) if len(sync) else np.nan
244+ out["llm_buy_share"] = float((d["sign"] > 0).mean())
245+ out["llm_pairwise_corr"] = pairwise_sign_corr(d)
246+ return out
247+
248+
249+def pairwise_sign_corr(d: pd.DataFrame, window: int = 20) -> float:
250+ """Average pairwise correlation of agents' net order sign aggregated per `window` ticks."""
251+ d = d.copy()
252+ d["w"] = d["t"] // window
253+ piv = d.pivot_table(index="w", columns="agent_id", values="sign", aggfunc="sum").fillna(0.0)
254+ if piv.shape[1] < 2 or piv.shape[0] < 3:
255+ return np.nan
256+ c = piv.corr().to_numpy()
257+ iu = np.triu_indices(piv.shape[1], 1)
258+ vals = c[iu]
259+ return float(np.nanmean(vals)) if np.isfinite(vals).any() else np.nan
260+
261+
262+def run_outcomes(res, bubble_thr: float = 0.05, bubble_min_len: int = 10, crash_thr: float = 0.10, crash_window: int = 20) -> dict:
263+ cfg = res.config
264+ out = {"name": cfg.name, "seed": cfg.seed, "config_hash": cfg.config_hash(), "n_traders": cfg.population.n_traders,
265+ "llm_share": cfg.population.llm_share, **{f"tr_{k}": v for k, v in cfg.treatment.items()}}
266+ out.update({k: res.manifest[k] for k in ("llm_share_traders", "llm_share_orders", "llm_share_volume", "n_llm_decisions", "n_trades", "wall_s")})
267+ out.update(series_outcomes(res.ticks, cfg.warmup, bubble_thr, bubble_min_len, crash_thr, crash_window, res.manifest.get("shocks")))
268+ out.update(agent_outcomes(res.agents, res.trades, res.llm, cfg.n_ticks))
269+ return out
added src/market_agents/signals/__init__.py +2 −0
@@ -0,0 +1,2 @@
1+"""Public and private information flows (news, announcements, macro/liquidity shocks)."""
2+from .news import NewsConfig, NewsFeed, NewsItem, ShockSchedule, Shock # noqa: F401
added src/market_agents/signals/news.py +75 −0
@@ -0,0 +1,75 @@
1+"""News arrivals and exogenous treatment shocks.
2+
3+* Public news: at random ticks a public item reveals x_t with noise (credible, low noise) or with a
4+ deliberate bias (misleading). Every agent can read public news; whether they act on it is policy.
5+* Private signals: drawn per agent at activation time by `private_signal` (see assets.fundamentals)
6+ with an agent-specific precision; informed agents receive them at heterogeneous arrival times.
7+* Shocks: scheduled treatment events — a jump in the fundamental mean (macro news), a change in the
8+ discount rate (monetary policy), a funding shock (leverage cap cut) or a liquidity shock (market
9+ makers withdraw). Shocks are logged so every treatment/control pair shares the same timing.
10+"""
11+from __future__ import annotations
12+
13+from dataclasses import dataclass, field
14+
15+import numpy as np
16+
17+
18+@dataclass(slots=True)
19+class NewsItem:
20+ time: int
21+ asset: int
22+ value: float # reported x (cents) possibly biased
23+ credible: bool
24+ noise_sd: float
25+ text: str = ""
26+
27+
28+@dataclass(slots=True)
29+class NewsConfig:
30+ rate: float = 0.02 # per-tick probability of a public news item (per asset)
31+ noise_sd: float = 1.0 # noise of credible news (cents)
32+ misleading_share: float = 0.0 # share of news items that are biased
33+ misleading_bias: float = 10.0 # bias (cents) applied with random sign
34+
35+
36+@dataclass(slots=True)
37+class Shock:
38+ time: int
39+ kind: str # 'fundamental_jump' | 'rate' | 'funding' | 'liquidity' | 'placebo'
40+ size: float = 0.0
41+ asset: int | None = None
42+ duration: int = 0
43+
44+
45+@dataclass(slots=True)
46+class ShockSchedule:
47+ shocks: list[Shock] = field(default_factory=list)
48+
49+ def at(self, t: int) -> list[Shock]:
50+ return [s for s in self.shocks if s.time == t]
51+
52+
53+class NewsFeed:
54+ def __init__(self, cfg: NewsConfig, rng: np.random.Generator, n_assets: int) -> None:
55+ self.cfg, self.rng, self.K = cfg, rng, n_assets
56+ self.items: list[NewsItem] = []
57+
58+ def step(self, t: int, x_true: np.ndarray) -> list[NewsItem]:
59+ out = []
60+ for k in range(self.K):
61+ if self.rng.random() < self.cfg.rate:
62+ misleading = self.rng.random() < self.cfg.misleading_share
63+ noise = self.rng.standard_normal() * self.cfg.noise_sd
64+ bias = (self.cfg.misleading_bias * (1 if self.rng.random() < 0.5 else -1)) if misleading else 0.0
65+ val = float(x_true[k] + noise + bias)
66+ direction = "above" if val > 0 else "below"
67+ item = NewsItem(time=t, asset=k, value=val, credible=not misleading, noise_sd=self.cfg.noise_sd,
68+ text=f"Analyst note: next dividend expected {abs(val):.1f} cents {direction} "
69+ f"its long-run mean.")
70+ out.append(item)
71+ self.items.extend(out)
72+ return out
73+
74+ def recent(self, t: int, window: int, asset: int | None = None) -> list[NewsItem]:
75+ return [n for n in self.items if t - window <= n.time <= t and (asset is None or n.asset == asset)]
added src/market_agents/utils/__init__.py +0 −0
added src/market_agents/utils/hashing.py +21 −0
@@ -0,0 +1,21 @@
1+from __future__ import annotations
2+import hashlib, json, subprocess
3+from pathlib import Path
4+
5+
6+def sha256_text(s: str) -> str:
7+ return hashlib.sha256(s.encode()).hexdigest()
8+
9+
10+def stable_json(obj) -> str:
11+ return json.dumps(obj, sort_keys=True, default=str, separators=(",", ":"))
12+
13+
14+def git_revision(path: str | Path | None = None) -> str:
15+ try:
16+ out = subprocess.run(["git", "rev-parse", "--short", "HEAD"], cwd=path, capture_output=True, text=True, timeout=5)
17+ dirty = subprocess.run(["git", "status", "--porcelain"], cwd=path, capture_output=True, text=True, timeout=5)
18+ rev = out.stdout.strip() or "unknown"
19+ return rev + ("-dirty" if dirty.stdout.strip() else "")
20+ except Exception:
21+ return "unknown"
added src/market_agents/utils/rng.py +20 −0
@@ -0,0 +1,20 @@
1+"""Seed management: one master seed -> named independent streams (common random numbers).
2+
3+Streams: 'fundamentals' (dividend path), 'news', 'slots', 'assign', 'llm_assign', 'agents' (one
4+sub-stream per agent id), 'activation', 'tiebreak'. A treatment that changes only `llm_share` leaves
5+every other stream's draws untouched, so matched pairs share fundamentals, news and endowments."""
6+from __future__ import annotations
7+
8+import numpy as np
9+
10+STREAMS = ("fundamentals", "news", "slots", "assign", "llm_assign", "activation", "tiebreak", "mock_llm", "shuffle")
11+
12+
13+def make_streams(seed: int) -> dict[str, np.random.Generator]:
14+ ss = np.random.SeedSequence(seed)
15+ children = ss.spawn(len(STREAMS))
16+ return {name: np.random.Generator(np.random.PCG64(c)) for name, c in zip(STREAMS, children)}
17+
18+
19+def agent_rng(seed: int, agent_id: int) -> np.random.Generator:
20+ return np.random.Generator(np.random.PCG64(np.random.SeedSequence([seed, 7_919, agent_id])))
added tests/__init__.py +0 −0
added tests/conftest.py +14 −0
@@ -0,0 +1,14 @@
1+import numpy as np
2+import pytest
3+
4+from market_agents.exchange.accounts import Account
5+from market_agents.exchange.engine import AssetSpec, Exchange, ExchangeConfig
6+
7+
8+@pytest.fixture
9+def exchange():
10+ ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(taker_fee_bps=2.0, maker_fee_bps=0.0, min_latency=0.05),
11+ np.random.default_rng(0), {0: 20000})
12+ for i in range(4):
13+ ex.register(Account(agent_id=i, cash=1_000_000, positions={0: 100}), latency=0.1 * (i + 1))
14+ return ex
added tests/integration/__init__.py +0 −0
added tests/integration/test_simulation.py +61 −0
@@ -0,0 +1,61 @@
1+import numpy as np
2+import pandas as pd
3+import pytest
4+
5+from market_agents.experiments.config import RunConfig
6+from market_agents.experiments.population import build_population
7+from market_agents.experiments.runner import run_config
8+from market_agents.utils.rng import make_streams
9+
10+
11+def test_end_to_end_classical_100_agents(tmp_path):
12+ cfg = RunConfig(name="t", seed=3, n_ticks=120, warmup=20)
13+ res = run_config(cfg, out_dir=tmp_path / "run")
14+ assert len(res.ticks) == 120 and res.manifest["n_trades"] > 50
15+ assert (tmp_path / "run" / "manifest.json").exists() and (tmp_path / "run" / "ticks.parquet").exists()
16+ # accounting: total wealth change equals dividends paid minus fees (mark-to-market at the same price)
17+ assert res.agents["final_wealth"].notna().all()
18+
19+
20+def test_determinism_across_runs():
21+ cfg = RunConfig(name="t", seed=11, n_ticks=100)
22+ a, b = run_config(cfg), run_config(cfg)
23+ pd.testing.assert_frame_equal(a.ticks, b.ticks)
24+ pd.testing.assert_frame_equal(a.trades, b.trades)
25+
26+
27+def test_common_random_numbers_across_llm_share():
28+ """Matched pairs: fundamentals and news paths are identical across llm_share treatments."""
29+ base = RunConfig(name="t", seed=5, n_ticks=80)
30+ tr = base.with_overrides(**{"population.llm_share": 0.2, "population.llm.provider_mix": {"mock": 1.0}})
31+ a, b = run_config(base), run_config(tr)
32+ assert np.allclose(a.ticks["full_value"], b.ticks["full_value"])
33+ sa, sb = build_population(base, make_streams(5)), build_population(tr, make_streams(5))
34+ # non-LLM slots keep their endowments and latencies
35+ for x, y in zip(sa, sb):
36+ assert x.cash == y.cash and x.latency == y.latency
37+ assert sum(s.policy_name == "llm" for s in sb) == 20
38+
39+
40+def test_mock_llm_run_reports_shares_and_validity():
41+ cfg = RunConfig(name="t", seed=7, n_ticks=120, population={"llm_share": 0.5, "llm": {"provider_mix": {"mock": 1.0}, "decision_interval": 10}})
42+ res = run_config(cfg, mock_kwargs={"invalid_rate": 0.2})
43+ m = res.manifest
44+ assert m["llm_share_traders"] == pytest.approx(0.5, abs=0.06) # market makers are never replaced
45+ assert 0 < m["llm_share_orders"] < 1 and m["n_llm_decisions"] > 100
46+ assert 0.6 < m["llm_valid_rate"] < 0.95
47+ assert set(res.llm["provider"]) == {"mock"}
48+
49+
50+def test_zero_trade_edge_case():
51+ cfg = RunConfig(name="t", seed=1, n_ticks=30, population={"mix": {"market_maker": 1.0}, "activity": {"market_maker": 0.0}})
52+ res = run_config(cfg)
53+ assert res.manifest["n_trades"] == 0 and len(res.ticks) == 30 and res.ticks["mid"].notna().all()
54+
55+
56+def test_shocks_are_logged_and_applied():
57+ cfg = RunConfig(name="t", seed=2, n_ticks=60, shocks=[{"time": 10, "kind": "fundamental_jump", "size": 30.0},
58+ {"time": 20, "kind": "liquidity"}, {"time": 30, "kind": "placebo"}])
59+ res = run_config(cfg)
60+ assert [s["shock"] for s in res.manifest["shocks"]] == ["fundamental_jump", "liquidity", "placebo"]
61+ assert res.ticks.loc[res.ticks.t == 10, "x"].iloc[0] > res.ticks.loc[res.ticks.t == 9, "x"].iloc[0] + 25
added tests/property/__init__.py +0 −0
added tests/property/test_lob_properties.py +64 −0
@@ -0,0 +1,64 @@
1+"""Property-based tests: random order streams never violate book, reservation or conservation invariants,
2+and the event log replays to the same final state."""
3+import numpy as np
4+from hypothesis import given, settings, strategies as st
5+
6+from market_agents.exchange.accounts import Account
7+from market_agents.exchange.engine import AssetSpec, Exchange, ExchangeConfig
8+from market_agents.exchange.orders import Action, CancelIntent, OrderIntent, OrderType, Side
9+
10+
11+def build(seed, n_agents=6, allow_short=False):
12+ ex = Exchange([AssetSpec(asset=0, name="A0", price_band_pct=0.9)], ExchangeConfig(taker_fee_bps=3.0, maker_fee_bps=-1.0,
13+ allow_short=allow_short, max_short_lots=50, order_ttl=7), np.random.default_rng(seed), {0: 1000})
14+ for i in range(n_agents):
15+ ex.register(Account(agent_id=i, cash=200_000 * (i + 1), positions={0: 50 * (i + 1)}, short_allowed=allow_short),
16+ latency=0.05 + 0.3 * (i % 3))
17+ return ex
18+
19+
20+intent = st.tuples(st.integers(0, 5), st.sampled_from([Side.BUY, Side.SELL]), st.sampled_from([OrderType.LIMIT, OrderType.MARKET]),
21+ st.integers(1, 80), st.integers(500, 1500), st.booleans())
22+
23+
24+@settings(max_examples=60, deadline=None)
25+@given(st.lists(intent, min_size=1, max_size=120), st.integers(0, 10_000))
26+def test_random_streams_keep_invariants(stream, seed):
27+ ex = build(seed)
28+ t = 0
29+ for k, (agent, side, otype, qty, price, cancel) in enumerate(stream):
30+ if cancel and ex.orders:
31+ oid = max(ex.orders)
32+ ex.submit(ex.orders[oid].agent_id, Action(cancels=[CancelIntent(oid)]), float(t))
33+ ex.submit(agent, Action(orders=[OrderIntent(0, side, otype, qty, None if otype is OrderType.MARKET else price)]), float(t))
34+ if k % 3 == 2:
35+ ex.process_until(float(t + 1))
36+ ex.end_of_tick(t)
37+ t += 1
38+ ex.books[0].check_invariants()
39+ ex.ledger.check_conservation()
40+ ex.process_until(float(t + 5))
41+ ex.books[0].check_invariants()
42+ ex.ledger.check_conservation()
43+ # reservations equal the sum over resting orders
44+ for acc in ex.ledger.accounts.values():
45+ resting = ex.books[0].resting_for(acc.agent_id)
46+ assert acc.reserved_cash == sum(o.reserved_cash for o in resting)
47+ assert acc.reserved_qty.get(0, 0) == sum(o.reserved_qty for o in resting)
48+ assert acc.positions[0] >= 0
49+
50+
51+@settings(max_examples=25, deadline=None)
52+@given(st.lists(intent, min_size=5, max_size=80), st.integers(0, 1000))
53+def test_determinism_and_replay(stream, seed):
54+ def run():
55+ ex = build(seed)
56+ for k, (agent, side, otype, qty, price, cancel) in enumerate(stream):
57+ ex.submit(agent, Action(orders=[OrderIntent(0, side, otype, qty, None if otype is OrderType.MARKET else price)]), float(k // 4))
58+ if k % 4 == 3:
59+ ex.process_until(float(k // 4 + 1))
60+ ex.process_until(1e9)
61+ return ex
62+ a, b = run(), run()
63+ assert a.snapshot() == b.snapshot()
64+ assert [(e.kind, e.payload) for e in a.events] == [(e.kind, e.payload) for e in b.events]
added tests/statistical/__init__.py +0 −0
added tests/unit/__init__.py +0 −0
added tests/unit/test_book.py +53 −0
@@ -0,0 +1,53 @@
1+from market_agents.exchange.book import LimitOrderBook
2+from market_agents.exchange.orders import Order, OrderType, Side
3+
4+
5+def mk(oid, side, qty, price, t=0.0, otype=OrderType.LIMIT):
6+ return Order(order_id=oid, agent_id=oid, asset=0, side=side, order_type=otype, qty=qty, price=price,
7+ submit_time=t, arrival_time=t)
8+
9+
10+def test_price_time_priority():
11+ b = LimitOrderBook(0)
12+ b.rest(mk(1, Side.SELL, 5, 101))
13+ b.rest(mk(2, Side.SELL, 5, 100))
14+ b.rest(mk(3, Side.SELL, 5, 100))
15+ inc = mk(9, Side.BUY, 7, 101)
16+ fills = b.match(inc)
17+ assert [(f.resting.order_id, f.qty, f.price) for f in fills] == [(2, 5, 100), (3, 2, 100)]
18+ assert inc.remaining == 0
19+ assert b.best_ask() == 100 and b.get(3).remaining == 3
20+ b.check_invariants()
21+
22+
23+def test_limit_does_not_cross_beyond_price():
24+ b = LimitOrderBook(0)
25+ b.rest(mk(1, Side.SELL, 5, 105))
26+ inc = mk(2, Side.BUY, 5, 104)
27+ assert b.match(inc) == [] and inc.remaining == 5
28+ b.rest(inc)
29+ assert b.best_bid() == 104 and b.best_ask() == 105
30+ b.check_invariants()
31+
32+
33+def test_market_order_walks_book_and_stops_when_empty():
34+ b = LimitOrderBook(0)
35+ b.rest(mk(1, Side.BUY, 3, 99))
36+ b.rest(mk(2, Side.BUY, 3, 98))
37+ inc = mk(3, Side.SELL, 10, None, otype=OrderType.MARKET)
38+ fills = b.match(inc)
39+ assert sum(f.qty for f in fills) == 6 and inc.remaining == 4
40+ assert b.best_bid() is None
41+ b.check_invariants()
42+
43+
44+def test_cancel_and_depth():
45+ b = LimitOrderBook(0)
46+ for i in range(1, 6):
47+ b.rest(mk(i, Side.BUY, i, 100 - i))
48+ bids, asks = b.depth(3)
49+ assert bids == [(99, 1), (98, 2), (97, 3)] and asks == []
50+ assert b.cancel(2).remaining == 2 and 2 not in b
51+ assert b.cancel(2) is None
52+ assert b.depth_within(Side.BUY, 2) == 1 + 3
53+ b.check_invariants()
added tests/unit/test_engine.py +104 −0
@@ -0,0 +1,104 @@
1+import numpy as np
2+import pytest
3+
4+from market_agents.exchange.accounts import Account
5+from market_agents.exchange.engine import AssetSpec, Exchange, ExchangeConfig
6+from market_agents.exchange.orders import Action, CancelIntent, OrderIntent, OrderStatus, OrderType, Side
7+
8+
9+def act(*intents):
10+ return Action(orders=list(intents))
11+
12+
13+def test_fill_moves_cash_and_shares_with_fees(exchange):
14+ ex = exchange
15+ ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 10, 20000)), 0.0)
16+ ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10, 20000)), 0.0)
17+ ex.process_until(1.0)
18+ a0, a1 = ex.ledger[0], ex.ledger[1]
19+ notional = 20000 * 10
20+ fee = round(notional * 2 / 10000)
21+ assert a0.cash == 1_000_000 + notional and a0.positions[0] == 90 # maker: no fee
22+ assert a1.cash == 1_000_000 - notional - fee and a1.positions[0] == 110 # taker pays 2 bp
23+ assert ex.ledger.exchange_fees == fee
24+ assert a1.reserved_cash == 0 and a0.reserved_qty[0] == 0
25+ ex.ledger.check_conservation()
26+ assert len(ex.trades) == 1 and ex.trades[0].aggressor is Side.BUY
27+
28+
29+def test_latency_orders_arrivals(exchange):
30+ ex = exchange
31+ # agent 3 (latency 0.4) submits first but agent 0 (latency 0.1) arrives first
32+ ex.submit(3, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 19000)), 0.0)
33+ ex.submit(0, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 19000)), 0.0)
34+ ex.process_until(1.0)
35+ q = ex.books[0].bids[-19000]
36+ assert [o.agent_id for o in q] == [0, 3]
37+
38+
39+def test_rejections(exchange):
40+ ex = exchange
41+ ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 1000, 20000)), 0.0) # only 100 held -> clipped to 100
42+ ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 5000)), 0.0) # outside price band
43+ ex.submit(2, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10_000, 20000)), 0.0) # insufficient cash
44+ ex.submit(3, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 0, 20000)), 0.0) # bad qty
45+ ex.process_until(1.0)
46+ reasons = [e.payload["reason"] for e in ex.events if e.kind == "reject"]
47+ assert reasons == ["price_band", "insufficient_cash", "bad_qty"]
48+ assert ex.orders[1].qty == 100 and ex.orders[1].status is OrderStatus.OPEN
49+ ex.ledger.check_conservation()
50+
51+
52+def test_cancel_releases_reservation(exchange):
53+ ex = exchange
54+ ids = ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 10, 19000)), 0.0)
55+ ex.process_until(1.0)
56+ assert ex.ledger[1].reserved_cash == 19000 * 10 + round(19000 * 10 * 2 / 10000) + 10 # notional + worst-case fee + 1c/lot
57+ ex.submit(1, Action(cancels=[CancelIntent(ids[0])]), 1.0)
58+ ex.process_until(2.0)
59+ assert ex.ledger[1].reserved_cash == 0 and ex.orders[ids[0]].status is OrderStatus.CANCELLED
60+ ex.ledger.check_conservation()
61+
62+
63+def test_market_buy_capped_by_cash():
64+ ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(taker_fee_bps=0.0), np.random.default_rng(0), {0: 100})
65+ ex.register(Account(agent_id=0, cash=10_000, positions={0: 0}), 0.1)
66+ ex.register(Account(agent_id=1, cash=0, positions={0: 1000}), 0.1)
67+ ex.submit(1, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 1000, 100)), 0.0)
68+ ex.submit(0, act(OrderIntent(0, Side.BUY, OrderType.MARKET, 500, None)), 0.5)
69+ ex.process_until(2.0)
70+ assert ex.ledger[0].positions[0] == 100 and ex.ledger[0].cash == 0
71+ ex.ledger.check_conservation()
72+
73+
74+def test_dividend_and_conservation(exchange):
75+ ex = exchange
76+ ex.pay_dividends(0, 50)
77+ assert all(a.cash == 1_000_000 + 5000 for a in ex.ledger.accounts.values())
78+ ex.ledger.check_conservation()
79+
80+
81+def test_halt_rejects_new_orders():
82+ ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(halt_threshold=0.05, halt_window=2, halt_duration=5),
83+ np.random.default_rng(0), {0: 10000})
84+ ex.register(Account(agent_id=0, cash=10_000_000, positions={0: 1000}), 0.1)
85+ ex.register(Account(agent_id=1, cash=10_000_000, positions={0: 1000}), 0.1)
86+ for t in range(3):
87+ ex.end_of_tick(t)
88+ ex.submit(0, act(OrderIntent(0, Side.SELL, OrderType.LIMIT, 10, 9000)), 3.0)
89+ ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.MARKET, 10, None)), 3.0)
90+ ex.process_until(4.0)
91+ assert ex.state[0].n_halts == 1 and ex.is_halted(0)
92+ ex.submit(1, act(OrderIntent(0, Side.BUY, OrderType.LIMIT, 1, 9000)), 4.0)
93+ ex.process_until(5.0)
94+ assert [e.payload["reason"] for e in ex.events if e.kind == "reject"] == ["halted"]
95+
96+
97+def test_margin_call_generates_liquidation():
98+ ex = Exchange([AssetSpec(asset=0, name="A0")], ExchangeConfig(margin_check=True), np.random.default_rng(0), {0: 10000})
99+ ex.register(Account(agent_id=0, cash=-900_000, positions={0: 100}, leverage_cap=5.0, maintenance_margin=0.25), 0.1)
100+ ex.register(Account(agent_id=1, cash=5_000_000, positions={0: 100}), 0.1)
101+ # equity = -900,000 + 100*10000 = 100,000 ; gross = 1,000,000 ; ratio 0.10 < 0.25 -> margin call
102+ actions = ex.margin_check()
103+ assert len(actions) == 1 and actions[0][0] == 0 and actions[0][1].orders[0].tag == "liquidation"
104+ assert any(e.kind == "margin_call" for e in ex.events)
added tests/unit/test_llm_validation.py +51 −0
@@ -0,0 +1,51 @@
1+import numpy as np
2+
3+from market_agents.agents.base import BookView, Observation, OwnOrderView
4+from market_agents.llm.policy import LLMPolicy, LLMPolicyConfig
5+from market_agents.llm.schema import parse_decision
6+
7+
8+def obs(cash=1_000_000, pos=100, open_orders=()):
9+ b = BookView(best_bid=19990, best_ask=20010, bids=[(19990, 10)], asks=[(20010, 10)], mid=20000.0, last_price=20000, halted=False)
10+ return Observation(t=10, agent_id=1, books={0: b}, mid_history={0: np.array([20000.0, 20005.0])}, recent_trades={0: []},
11+ last_dividend={0: 50.0}, public_value={0: 20000.0}, x_hat_public={0: 0.0}, news=[], private_signals={}, signal_precision={},
12+ cash=cash, free_cash=cash, positions={0: pos}, equity=cash + pos * 20000, open_orders=list(open_orders),
13+ leverage_cap=1.0, short_allowed=False, max_order_qty=50, tick_cents={0: 1}, div_interval=20,
14+ ticks_to_dividend=10, r_period=0.0025)
15+
16+
17+def pol():
18+ return LLMPolicy(LLMPolicyConfig(models={"mock": "mock-v1"}, use_cache=False))
19+
20+
21+def test_parse_lenient_and_strict():
22+ d, err = parse_decision('```json\n{"action":"hold"}\n```')
23+ assert d is not None and d.action == "hold" and err == ""
24+ d, err = parse_decision("not json")
25+ assert d is None and err.startswith("json")
26+ d, err = parse_decision('{"action":"place","quantity":-1}')
27+ assert d is None and err.startswith("schema")
28+
29+
30+def test_validation_rules():
31+ p = pol()
32+ d, _ = parse_decision('{"action":"place","side":"buy","order_type":"limit","quantity":10,"limit_price":200.05}')
33+ a, ok, why = p.to_action(d, obs())
34+ assert ok and a.orders[0].price == 20005 and a.orders[0].qty == 10
35+ d, _ = parse_decision('{"action":"place","side":"buy","order_type":"limit","quantity":10,"limit_price":300.0}')
36+ assert not p.to_action(d, obs())[1] # outside ±20 %
37+ d, _ = parse_decision('{"action":"place","side":"buy","order_type":"market","quantity":60}')
38+ assert "max" in p.to_action(d, obs())[2]
39+ d, _ = parse_decision('{"action":"place","side":"buy","order_type":"market","quantity":10}')
40+ assert not p.to_action(d, obs(cash=1000))[1] # cannot afford
41+ d, _ = parse_decision('{"action":"place","side":"sell","order_type":"market","quantity":10}')
42+ assert not p.to_action(d, obs(pos=0))[1] # no shares, no shorting
43+ d, _ = parse_decision('{"action":"cancel","cancel_order_ids":[5]}')
44+ assert not p.to_action(d, obs())[1]
45+ assert p.to_action(d, obs(open_orders=[OwnOrderView(5, 0, 1, 10, 10, 19990, 3.0)]))[1]
46+
47+
48+def test_budget_cap_halts_calls():
49+ p = LLMPolicy(LLMPolicyConfig(models={"mock": "mock-v1"}, use_cache=False, usd_cap=0.0))
50+ a, rec = p.decide_one(obs(), "mock", "mock-v1", "generalist")
51+ assert not rec.valid and "cap" in rec.error and p.halted_for_budget
added uv.lock +1614 −0
@@ -0,0 +1,1614 @@
1+version = 1
2+revision = 3
3+requires-python = ">=3.12"
4+resolution-markers = [
5+ "python_full_version >= '3.14' and sys_platform == 'win32'",
6+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
7+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
8+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
9+ "python_full_version < '3.13' and sys_platform == 'win32'",
10+ "python_full_version == '3.13.*' and sys_platform == 'emscripten'",
11+ "python_full_version < '3.13' and sys_platform == 'emscripten'",
12+ "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
13+ "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'",
14+]
15+
16+[[package]]
17+name = "annotated-types"
18+version = "0.8.0"
19+source = { registry = "https://pypi.org/simple" }
20+sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
21+wheels = [
22+ { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
23+]
24+
25+[[package]]
26+name = "anthropic"
27+version = "2.3.0"
28+source = { registry = "https://pypi.org/simple" }
29+dependencies = [
30+ { name = "anyio" },
31+ { name = "docstring-parser" },
32+ { name = "httpx2" },
33+ { name = "jiter" },
34+ { name = "opentelemetry-api" },
35+ { name = "pydantic" },
36+ { name = "sniffio" },
37+ { name = "typing-extensions" },
38+]
39+sdist = { url = "https://files.pythonhosted.org/packages/3e/ba/4c91947e0d0062d66d371db371597287153f7685fbed512d3294d0cde3c7/anthropic-2.3.0.tar.gz", hash = "sha256:edfdea1e3fa7b3ce25df1511953408b2f03bfa61eb6f6268f766101a9e18ad53", size = 1869656, upload-time = "2026-10-23T23:18:47.649Z" }
40+wheels = [
41+ { url = "https://files.pythonhosted.org/packages/3f/29/eb3558ab6bf252a07ac41c76ac5b0ea24a0aa0c23f87a40aaf31a5f8dba4/anthropic-2.3.0-py3-none-any.whl", hash = "sha256:aa6a3984f9276f307933d42dc6f58b6efc821a5ecafbc8d1bb5993a066767622", size = 2216980, upload-time = "2026-10-23T23:18:45.779Z" },
42+]
43+
44+[[package]]
45+name = "anyio"
46+version = "4.15.1"
47+source = { registry = "https://pypi.org/simple" }
48+dependencies = [
49+ { name = "idna" },
50+ { name = "typing-extensions", marker = "python_full_version < '3.15'" },
51+]
52+sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
53+wheels = [
54+ { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
55+]
56+
57+[[package]]
58+name = "colorama"
59+version = "0.4.6"
60+source = { registry = "https://pypi.org/simple" }
61+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
62+wheels = [
63+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
64+]
65+
66+[[package]]
67+name = "contourpy"
68+version = "1.4.0"
69+source = { registry = "https://pypi.org/simple" }
70+dependencies = [
71+ { name = "numpy" },
72+]
73+sdist = { url = "https://files.pythonhosted.org/packages/83/5a/a55177dd22553a277388e8a1b3220e92de91bacb28356cdc73caa240121d/contourpy-1.4.0.tar.gz", hash = "sha256:20156f5a1ac4f8ce02656e39a61e82164a3d359796dc8026f75b062783d500e1", size = 13323726, upload-time = "2026-09-11T19:05:05.808Z" }
74+wheels = [
75+ { url = "https://files.pythonhosted.org/packages/97/ba/01bde9753bdbba04a6da9d2bff3881f021bc708f654655e95fae26d3b3a3/contourpy-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:186ba929df36d61b6127da2e89cd1357e4cb79aca647381ab1a6feb0b152877b", size = 297302, upload-time = "2026-09-11T19:02:45.103Z" },
76+ { url = "https://files.pythonhosted.org/packages/dd/f5/c5bf67522d49a2222f3154fe46879451d26f0e35ece41770758a64ea78cb/contourpy-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c76f3a5318164db7d9401132fc1b5364b784c613c93fa506e3b5e6d1bf353ec8", size = 284921, upload-time = "2026-09-11T19:02:48.049Z" },
77+ { url = "https://files.pythonhosted.org/packages/0f/cc/cb989599eec12fda312e127eb8e04a8b21a7a6c94bf7ff1bc68273334a42/contourpy-1.4.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43c3ccbb32c6294b183dcc8e8c46dacf5ecef809497e3d48a5be298eef185ad0", size = 356332, upload-time = "2026-09-11T19:02:51.408Z" },
78+ { url = "https://files.pythonhosted.org/packages/47/fb/6f620d7602507817b0b4c21dd790ed68688f1f4ae9c29aacf21f08db5cf1/contourpy-1.4.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86d05cec773c9507a3950122e0e40ce77c23c75ceeb2fc189514e71893cbb34b", size = 405878, upload-time = "2026-09-11T19:02:54.093Z" },
79+ { url = "https://files.pythonhosted.org/packages/32/4e/693c6d6bdece679f0953775eef3a1b66d56be55478d936e7deb1f3004422/contourpy-1.4.0-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2deb580178ca19437a84bd77e4bc2cd91a8ccad212413a83c273900868b5978c", size = 408296, upload-time = "2026-09-11T19:02:56.528Z" },
80+ { url = "https://files.pythonhosted.org/packages/78/f9/b6831508960d559581c448532ba4df312217072975486dbf2b24fcc5b76f/contourpy-1.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:875f42444c9cf48d56f724f2637e60d0f73b3b12c9041e1484580a233edf9591", size = 383243, upload-time = "2026-09-11T19:02:58.638Z" },
81+ { url = "https://files.pythonhosted.org/packages/f2/21/52903825a0ae7bb625e8bd30a09816d4c3c5d6174a469c10a616166cf780/contourpy-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f863c6100bf926cf47d13f3cd75f9bb8ebd98aaab230eeb4468b91f98f39a6b3", size = 1357306, upload-time = "2026-09-11T19:03:01.036Z" },
82+ { url = "https://files.pythonhosted.org/packages/d8/79/d68b1ce8539e4071518fed9523f558398f34dcd078b8927b109c72dad2ef/contourpy-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3863ef2e2b13fe93f8c0ebb08ee400cb07153b8b0e91c5acb26e4537f283634f", size = 1427243, upload-time = "2026-09-11T19:03:03.866Z" },
83+ { url = "https://files.pythonhosted.org/packages/ac/f0/b75a10e9d0616b97a30de277b6dfe83f1879880854c7330337309530eb48/contourpy-1.4.0-cp312-cp312-win32.whl", hash = "sha256:5450f091ac1be0be3ad3a2a3b3f23b5e443e78c670ced4fd347d626f92a28fd2", size = 347345, upload-time = "2026-09-11T19:03:06.192Z" },
84+ { url = "https://files.pythonhosted.org/packages/50/a9/dab08786bb4d77ef9a046b3c1be923be4e73c5d07d91f5a54e8ea9b09e41/contourpy-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e697d94e69f499ff6bebb899cae97a58d5d14f0e1fe9568b43a0248d2f9af8c", size = 234083, upload-time = "2026-09-11T19:03:08.122Z" },
85+ { url = "https://files.pythonhosted.org/packages/47/b9/3ba509755a970dbde5948e7142ac21f266be72f38017cc4087a05fdceee1/contourpy-1.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:0c7a4c2716a4e98342221954416a836cca77c996c14ddbf22b5f01d5d93ca09c", size = 559521, upload-time = "2026-09-11T19:03:10.047Z" },
86+ { url = "https://files.pythonhosted.org/packages/b7/8b/62a6eacea08ea0b6ab0159a7783df7ed421ff9c1ff58fef7c31f58fa5e52/contourpy-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e21d1d4a9db5b3a793649c7ab0ddb4697f2818929eec871a7b9c45fce7b2a1ea", size = 297330, upload-time = "2026-09-11T19:03:11.809Z" },
87+ { url = "https://files.pythonhosted.org/packages/3f/2f/103cdc5b7077cd569989d3ddb99ee3d4e96ee31bde8df2a6e426b2dc6195/contourpy-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:196759a3e4db60e1546167e361090ebb6d199aa3aafc9e34ea17a5ac3b23e814", size = 285037, upload-time = "2026-09-11T19:03:13.781Z" },
88+ { url = "https://files.pythonhosted.org/packages/bf/25/97001d1fdf940e6f5adfc592d4f5899fff95daf7dceead3988fe09987d2d/contourpy-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c0e07c691f3b3321913ed9b8161c50ea5f77fadd006f52f5e755359b0dcbfc5", size = 356675, upload-time = "2026-09-11T19:03:15.374Z" },
89+ { url = "https://files.pythonhosted.org/packages/a6/5e/9a7241a89e2f74402aefa2569e1349ad2750ff77823c04c788eaaedb7fdd/contourpy-1.4.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f861af508fca2384eef392bc1d8377cfa349b2b854cb11b45d89c75780e2561", size = 406990, upload-time = "2026-09-11T19:03:17.44Z" },
90+ { url = "https://files.pythonhosted.org/packages/36/81/3a4fbbf6a806eada5453a6d16f6c7d96b9f4b3e57a3127368fb545986b8a/contourpy-1.4.0-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a03b32c2e7eda8b17757c022162c13c86f5c3d6f937dddf9ba3c3ff7b513953b", size = 407271, upload-time = "2026-09-11T19:03:19.523Z" },
91+ { url = "https://files.pythonhosted.org/packages/68/77/8f93ecbde1a4d11dfd460cd19a0c159f5487352e751fa87f5d78cc9db08e/contourpy-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1219a8898523cba821085f2da8a1b962cc696325763f09d7f93538ba43b2d70", size = 384707, upload-time = "2026-09-11T19:03:21.208Z" },
92+ { url = "https://files.pythonhosted.org/packages/05/07/e5cdf94d68961ee464de6b4fedd3d2f666584f59ccd124bb1290597122ab/contourpy-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20da2f86bfaed60dbba729fe738c8241b1dd22b6cf7e8cbbf30df290bd04ba28", size = 1356741, upload-time = "2026-09-11T19:03:23.621Z" },
93+ { url = "https://files.pythonhosted.org/packages/fd/8a/2301d07accb257ac08daeb631875fada06fedd019c20ff7b00ee07e9d211/contourpy-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ced1670fafee703b8277ab1645072a226e74f167166eae146fb743916119b67", size = 1426793, upload-time = "2026-09-11T19:03:25.955Z" },
94+ { url = "https://files.pythonhosted.org/packages/7d/5d/a62648f06259d15c139b14eb8cf3c58fe53cb970a55f2bb77d1070e80a41/contourpy-1.4.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:abd0f7e51ecc52bf8c95713bd80b1c17c516a71fa391c54475308a4377903c2b", size = 117961, upload-time = "2026-09-11T19:03:27.713Z" },
95+ { url = "https://files.pythonhosted.org/packages/71/31/c5b7771ba6982f319ec90c0cf5c425b36891adfeee2d9f667f90b2d0c166/contourpy-1.4.0-cp313-cp313-win32.whl", hash = "sha256:510f7d93d94cf6ebf4e2cd0640bea16ab8affac7547230175b6745b596ce0599", size = 347438, upload-time = "2026-09-11T19:03:29.829Z" },
96+ { url = "https://files.pythonhosted.org/packages/68/c1/a59df0754bf83a6f3e153cc19d550a5b01f607e389b1c4d57f436f9139e0/contourpy-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:6507a016976a75a15aee47809a34605de6244ccc056e6164bf9fb14b27eecad4", size = 234084, upload-time = "2026-09-11T19:03:31.657Z" },
97+ { url = "https://files.pythonhosted.org/packages/41/48/97224f5decb1efa5a610f912195deb45321f73ec9b9dac485cd6fe2d5a6a/contourpy-1.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:423bd5f4b3f11d54a8c597234e513382e3454bb106b8ec7ae32ba4ed63f8d99b", size = 559559, upload-time = "2026-09-11T19:03:33.528Z" },
98+ { url = "https://files.pythonhosted.org/packages/75/b0/b548628c8dddf6e5e0a981b7fa8e4c008024df09c7d17c6a9e271edfa0b3/contourpy-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a3e67bc1a6a4618a1dac7c3053a9ffece5ddfa2046b2670b342cf430bb0b87d1", size = 297354, upload-time = "2026-09-11T19:03:35.017Z" },
99+ { url = "https://files.pythonhosted.org/packages/04/5a/513484208742f65af2648c519f0b7ab034616e183c4403538024c136a1ab/contourpy-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a702e2e178f4fcf96f04775d0c059e4c917925abf0d3208ebb6865df3c23d", size = 283729, upload-time = "2026-09-11T19:03:36.818Z" },
100+ { url = "https://files.pythonhosted.org/packages/35/da/5a6562febc994b2c4bf9d01e57a50458ef8a2051bb3e7b35a00333e0fc26/contourpy-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:404dbcd9233513dfd1323b66ea593fef90e899f658f6b8a9ed8e93bd0ca669db", size = 357819, upload-time = "2026-09-11T19:03:39.058Z" },
101+ { url = "https://files.pythonhosted.org/packages/6d/99/7b358ce888aaa426f755daa096c99c59d17c352037344e836f20e5d3b957/contourpy-1.4.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61624f6722480aa7e168fd746164e7bfdf48f8ccb6542f1613741200dc37e2b1", size = 407795, upload-time = "2026-09-11T19:03:41.293Z" },
102+ { url = "https://files.pythonhosted.org/packages/be/02/34d7548adf08c60967435d79d7a90882eacc84f5093435287974c7e99943/contourpy-1.4.0-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e741a39dfc96babe1722561e81351aeec104526c57dbe91c167c1db606c2d55", size = 408552, upload-time = "2026-09-11T19:03:42.728Z" },
103+ { url = "https://files.pythonhosted.org/packages/cf/8c/9677dad226e5aaac5b823d5b09d881ebc32ff1648af417597649841e516c/contourpy-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e439ab450c93455feb0218532ded3e946ec7a9b5f459069f122cfa44b89229f6", size = 384850, upload-time = "2026-09-11T19:03:44.402Z" },
104+ { url = "https://files.pythonhosted.org/packages/08/f5/e3fbeaa489c223629fbb434737cb3de4ecaca9cae4825e77b92490f640f8/contourpy-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:417be048f7e122cefbe2a34c51a1f2b0410f8eb35796f794d45475ddb7872d9d", size = 1357601, upload-time = "2026-09-11T19:03:46.746Z" },
105+ { url = "https://files.pythonhosted.org/packages/88/be/55fec293d342c222ed58a5d4056df4362e192af2e59527a3d5d822de8828/contourpy-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43d80072a32299bf945de0a6dd4e034ea0162e832261de2e07a274a5adbba9c9", size = 1427340, upload-time = "2026-09-11T19:03:48.745Z" },
106+ { url = "https://files.pythonhosted.org/packages/86/40/0faabf453edf59b0e3633381d7ca72c3414fb9b72b5cbb08b06ed792d3e5/contourpy-1.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:fa1b787362a3856e63dd2b89449f6c384d55ee1beb8f94f4bcc871b40f02462c", size = 118355, upload-time = "2026-09-11T19:03:50.047Z" },
107+ { url = "https://files.pythonhosted.org/packages/38/0c/7ea75aa3559ae8a6805015e15a19c92b6480752ecc92526361e5776e88c5/contourpy-1.4.0-cp314-cp314-win32.whl", hash = "sha256:738c44fa71735a617f36da58e32810512407d283d5d1bb5b1cecb00d7eeba7cf", size = 356977, upload-time = "2026-09-11T19:04:11.401Z" },
108+ { url = "https://files.pythonhosted.org/packages/15/41/3df8cc14bb572c8b447f9dfbc876703f0f2f2896058285b5c87a1d4a638d/contourpy-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:181bea01bc742734ae672fa00c717d855dd35e1f029536406538c52e7b0cd73d", size = 240101, upload-time = "2026-09-11T19:04:12.772Z" },
109+ { url = "https://files.pythonhosted.org/packages/66/01/ee6830a5aa4565662a345172f6f35eeeed625d20f1c9d4ecd35318fe4ade/contourpy-1.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:64039341e2d8804f1a13bda8c69083eab935167bbd7e856cf096241f17d5bd45", size = 576359, upload-time = "2026-09-11T19:04:14.983Z" },
110+ { url = "https://files.pythonhosted.org/packages/51/af/49ee9c9cf012699e505c1b63c531cc99f19df50f205173092c4d541e3ec7/contourpy-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ef47fef9a912c77e3d84b014f701e5a9340f25703e9ed3bcebe37f91fb69dc49", size = 313601, upload-time = "2026-09-11T19:03:51.452Z" },
111+ { url = "https://files.pythonhosted.org/packages/ad/59/ce411ab2be0f805038626d8f4ca9ce563487ff51ff6c363e7512271564ed/contourpy-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:96019f059bcb3774acc0104be3360a57b36d33eaaa2e1d7f77c800ca8e07618b", size = 302812, upload-time = "2026-09-11T19:03:53.022Z" },
112+ { url = "https://files.pythonhosted.org/packages/95/b0/390915f9af14c1e2d3b2a9ad99d85b98df4352346a51e368cbb825f813c3/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddf5a2596d716fd3793434844caf31abc7a40b1e8718431420c89858268fb909", size = 348996, upload-time = "2026-09-11T19:03:54.594Z" },
113+ { url = "https://files.pythonhosted.org/packages/c7/85/e0952576d54322f3e9983b1e049a7a31f42ce82c5c29d01821e6dd3780d7/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd59df9e2fb0aff8bd7fd1adb9f349b7c835245a9d0f18c2f2deeb537190f2b1", size = 400272, upload-time = "2026-09-11T19:03:56.822Z" },
114+ { url = "https://files.pythonhosted.org/packages/d6/7d/f13543a5e1598e4a4ad01d9d5e2d65f9c09c37660616429913d606c5977d/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7794ea07cab575633daad8d6e963b662053391022294d3aacc1d9ba9ef54114", size = 412895, upload-time = "2026-09-11T19:03:58.487Z" },
115+ { url = "https://files.pythonhosted.org/packages/c5/a8/6e5eaff53507dfd2dfc428240eecda32453067277d4b3e7ca71210b2d611/contourpy-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c8a74744fa746eeaa12f0f9ed7f8b4add9d8f1b14d95e3b5e133675eac888f", size = 377060, upload-time = "2026-09-11T19:04:00.122Z" },
116+ { url = "https://files.pythonhosted.org/packages/16/a3/d00e44d35511b3cead5bd794ee672d4eecf30d9de4ceac57a49d6204064f/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:107ec46f7aa1664266d69b1181aadc953d58d39f7d8d648fa5b795d4a060b0de", size = 1345688, upload-time = "2026-09-11T19:04:02.705Z" },
117+ { url = "https://files.pythonhosted.org/packages/1e/01/1e533b0cae32c8edd63fe40435ec3b29345dab87321afcd28722d7f957d1/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c89c7f38a0d7a94356d74e3391073c4325c4d47ad2fd065c3fe7dbae16c0c", size = 1416140, upload-time = "2026-09-11T19:04:04.77Z" },
118+ { url = "https://files.pythonhosted.org/packages/6d/0c/4ee66759f00d6a5d6909782f14f441d639fa18a4a43142e334351efc47f8/contourpy-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:912c6afaa106e2f74ba22b30416b77ef7eb94e3bdc5a4df51ab147845dba9b6d", size = 371982, upload-time = "2026-09-11T19:04:06.233Z" },
119+ { url = "https://files.pythonhosted.org/packages/e6/ac/b3e5324138c3e3741f526663f4f265f4f69dfdf26415a39428940864a5f6/contourpy-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ef9440f6f8506246269a82734f5ff9e2e4c5e775b3996fc883cc491c5257eca6", size = 262907, upload-time = "2026-09-11T19:04:07.882Z" },
120+ { url = "https://files.pythonhosted.org/packages/19/37/c9aa45e47819dc15a38fc5c81a2fb987fde55e9d3b991fbde514e3b6b5f5/contourpy-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:fc9feef8f1f001c5b87decadc67c4a5d1eebb62ca39c4763d1237ff62cf2b707", size = 587071, upload-time = "2026-09-11T19:04:09.898Z" },
121+ { url = "https://files.pythonhosted.org/packages/cc/74/66d4e6fbea3bcb3fd4e5ae21c039ba31b8fc408b35884b4bc09b88016dce/contourpy-1.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:79b06c60d5e569ce12c5da8f0c51217cdc484386e2349fa717c30fafb56d2871", size = 297358, upload-time = "2026-09-11T19:04:16.718Z" },
122+ { url = "https://files.pythonhosted.org/packages/2e/8d/8f7f25e71d73f15d7859943189900d2d348e740388c9ef52a0180d284762/contourpy-1.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f963ea9f0d68d2b6a02d861e3c0aae09ae25ba21f243fcf281e9fed468b078d", size = 283732, upload-time = "2026-09-11T19:04:18.087Z" },
123+ { url = "https://files.pythonhosted.org/packages/f0/90/957549e3b03c407c670e3e191db63f4aef60dab6605bc967af3a806b543e/contourpy-1.4.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5545ff38229c15d11d822fc6f7415b932da9f4975f53e305b7b96ebfb01f889c", size = 357820, upload-time = "2026-09-11T19:04:20.077Z" },
124+ { url = "https://files.pythonhosted.org/packages/56/4c/fa8de87d907b41b05d03f4945a51675cd67319650d01b4c927d209ffa6ee/contourpy-1.4.0-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630d32f06cedf37b7f1dd2b6a12e4492826bd477a4c9c7b2e2fee6ce3a2cea76", size = 407804, upload-time = "2026-09-11T19:04:21.781Z" },
125+ { url = "https://files.pythonhosted.org/packages/04/e7/96bd4f4a4ae285727befbdb4b332d53a2654cc5da4585d2525b47a063e1f/contourpy-1.4.0-cp315-cp315-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be85d160e7c795113c2a93254dde2fc7e86b375322570eee78b420969426973c", size = 408559, upload-time = "2026-09-11T19:04:23.432Z" },
126+ { url = "https://files.pythonhosted.org/packages/ae/94/39660e8dc309f96bf7971eb4ec5659f272f84e42b34c9990a96ecce8442c/contourpy-1.4.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:018227e134b73090b06f911e773a526c77c9af426be29065500ce096a5accd4c", size = 384861, upload-time = "2026-09-11T19:04:25.198Z" },
127+ { url = "https://files.pythonhosted.org/packages/ab/7b/411dccbf73d5d19da49bf7735786b3baf4ddb1456412d48ae4d4ab1fc761/contourpy-1.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4bffcb2e8cc5c0e837631acab59dd04265fdac376550e7c3e310523877c68cb7", size = 1357612, upload-time = "2026-09-11T19:04:27.614Z" },
128+ { url = "https://files.pythonhosted.org/packages/a0/39/bf2442130e7d5aae75a4de4504f0c8aca3ef3da3feac3940ee27ca985d83/contourpy-1.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc87f2ffa84a49a77d76cb792f28a48ff3b05a222aa0b2bddbd839ba78e1d5ca", size = 1427345, upload-time = "2026-09-11T19:04:29.646Z" },
129+ { url = "https://files.pythonhosted.org/packages/96/60/e5d9196dd83ccd7dd103762982b10ef19a4f72e8ef67ae7157699b9be0bb/contourpy-1.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:c8be3392b84cf43373a488874dc1ebf46f60784a70977f92586e18019d71c7ed", size = 118157, upload-time = "2026-09-11T19:04:31.091Z" },
130+ { url = "https://files.pythonhosted.org/packages/1a/b2/ae3d90f08ba2ed82189d93b55ffbd41e559760e89af4f9ef54d51869f41d/contourpy-1.4.0-cp315-cp315-win32.whl", hash = "sha256:618137ec5778fb76d494c9ec854b104a377fddba0d128bef7136cce2c3bf0df8", size = 356982, upload-time = "2026-09-11T19:04:52.913Z" },
131+ { url = "https://files.pythonhosted.org/packages/02/a0/36648fff407dcf8b8482985d33367ffc0fe90be72383869312cd0bd00a1b/contourpy-1.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:ea09dc704029930cbd097f75cb0cc1db9852adbcf1a151fac7e9559f7bcbfd19", size = 240101, upload-time = "2026-09-11T19:04:54.587Z" },
132+ { url = "https://files.pythonhosted.org/packages/07/09/49e987a96706b8f877596cee00d558d7880793a833addc6b33f80805e5dc/contourpy-1.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:a5e2bb871b90cd7a52bdee63bf80ef7acb46398d27efc66d7679240016efc5aa", size = 576375, upload-time = "2026-09-11T19:04:56.515Z" },
133+ { url = "https://files.pythonhosted.org/packages/e3/18/820ba24a070fe2a834af73e52245c501960746f5c1777ebc64ec0c0dec12/contourpy-1.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:432e89f835cf01f8d2123a130a3126b17ee6e0348e4d62c0973872b30aa873bf", size = 313543, upload-time = "2026-09-11T19:04:32.582Z" },
134+ { url = "https://files.pythonhosted.org/packages/d3/4c/f61a63d8107e0c1c3fa47d30ce45abad90b2ad48a1a9a4510332db7e0176/contourpy-1.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e0a2392533e5e2abdf277c951047516acc8d2f3c42b2dd9dc34c907c9ebabf2", size = 302702, upload-time = "2026-09-11T19:04:34.257Z" },
135+ { url = "https://files.pythonhosted.org/packages/9d/93/5af95cb5113ecc6e780ca0c6c79756a2cea8833f971bbae112dd4ad73ded/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:758e7496cec3195fc28a28945eab3ceef348c47fb95c7afc2eca2f3901209c8c", size = 354370, upload-time = "2026-09-11T19:04:36.158Z" },
136+ { url = "https://files.pythonhosted.org/packages/47/75/5a9942446647202beb7b4d150891bfcb78fb112c73862484bbe4cc031092/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bee6ff96653e0807cd0f8eaef00174a031e3a2effa5c20f49a5a8d574b05af23", size = 402340, upload-time = "2026-09-11T19:04:37.914Z" },
137+ { url = "https://files.pythonhosted.org/packages/c3/4f/bfdb0c67775a584f957211936661289789c4d0ec3e988bca2c8c02f8090e/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a58b5f130afec61a093d743cdda435cc4047748b6791627e34a68f41ec3a9b07", size = 410721, upload-time = "2026-09-11T19:04:39.925Z" },
138+ { url = "https://files.pythonhosted.org/packages/65/d3/dddf13d6bb26e145c99debd1f2695018b5f3e24452dd9bfc9904c9d2ec89/contourpy-1.4.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9dc493e7924e5aa32d89d0b8e50986280f3d4b284968a792787bd7bcffd3cad", size = 382604, upload-time = "2026-09-11T19:04:41.793Z" },
139+ { url = "https://files.pythonhosted.org/packages/37/b4/89f0a5f6c6ccd322eae2704c7e933c82a4e594a61773933b4b0a18646866/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c927ec747633e68c9920bcaad48b0bcefe648812b822f18c34e2d0e2270a3d2a", size = 1354996, upload-time = "2026-09-11T19:04:43.856Z" },
140+ { url = "https://files.pythonhosted.org/packages/5d/29/fc91cc1ac6f3b591a269f4357b656d6ed0c39eb17c148f41f6da2f9729b9/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:80c7fc8e7ac217777ce7ba2db3cf8478652b5a4b335283111012ff9eaac9d842", size = 1425030, upload-time = "2026-09-11T19:04:45.981Z" },
141+ { url = "https://files.pythonhosted.org/packages/d8/b8/bb129f524210c5d3eb8616876e02af280fba8b5554e0ac1afad80b51e06b/contourpy-1.4.0-cp315-cp315t-win32.whl", hash = "sha256:90feb8da006803a95ba573fddc9536357d0c3faddc3a4c9e22816ca49af52878", size = 371854, upload-time = "2026-09-11T19:04:48.106Z" },
142+ { url = "https://files.pythonhosted.org/packages/d8/b1/bc774d9d200cfc4f073bba78b1bb1cb3b55bf1c8c345eae41fc2db510459/contourpy-1.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:de503609fdb71f597634ca64fb4bd2f13d27c97f140e912b1e8ed93544840df0", size = 262450, upload-time = "2026-09-11T19:04:49.502Z" },
143+ { url = "https://files.pythonhosted.org/packages/54/bf/eb4ff492e1c3d027617b5a20e4e6d63a035b41b8cf77a95950159556a3a5/contourpy-1.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:78ed5c5f962b3e156109f0531b174a65ba80d13cc681a70227b12524c162e184", size = 587064, upload-time = "2026-09-11T19:04:51.148Z" },
144+]
145+
146+[[package]]
147+name = "cycler"
148+version = "0.12.1"
149+source = { registry = "https://pypi.org/simple" }
150+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
151+wheels = [
152+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
153+]
154+
155+[[package]]
156+name = "docstring-parser"
157+version = "0.18.0"
158+source = { registry = "https://pypi.org/simple" }
159+sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
160+wheels = [
161+ { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
162+]
163+
164+[[package]]
165+name = "duckdb"
166+version = "2.0.0"
167+source = { registry = "https://pypi.org/simple" }
168+sdist = { url = "https://files.pythonhosted.org/packages/c2/7c/dfdf5e9cebefd032f57f09b224993e861ecb55f61e36c709544b04b44f70/duckdb-2.0.0.tar.gz", hash = "sha256:09de511ef0dc76a5213e8d3d87a8b89bb4d3e8a6fe73a9f60dc001269946b44f", size = 16771077, upload-time = "2026-10-21T12:51:13.908Z" }
169+wheels = [
170+ { url = "https://files.pythonhosted.org/packages/41/28/1fb52f9ea10205a55a07c32e37cd4fd68fa9d1dc2520a12405ec9bb59cac/duckdb-2.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cb1cc30318397c3bcffa6b683196293698052588de3c1ba1e144ba0c72560ce9", size = 37003294, upload-time = "2026-10-21T12:50:56.489Z" },
171+ { url = "https://files.pythonhosted.org/packages/07/5b/fbe92644709fb9162cc51753f92f892b1f333658079d1f11efaed43341d2/duckdb-2.0.0-cp312-abi3-macosx_10_15_x86_64.whl", hash = "sha256:46c7a299ff5f45be9068cfc0ceecb3fd574af9535de8cb6908ae8b52d2846cbd", size = 19689207, upload-time = "2026-10-21T12:50:59.331Z" },
172+ { url = "https://files.pythonhosted.org/packages/8f/a3/4d23938223626980cdcf18fd6b0dbf040823b5cf15beabbf4ab7ee276631/duckdb-2.0.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:b5d758b53751c930e09d371230241005ea5203c8edc65a1e1fafca83fcc906ef", size = 17597612, upload-time = "2026-10-21T12:51:01.818Z" },
173+ { url = "https://files.pythonhosted.org/packages/92/4a/985fb9d93d7f9f8e82092ddfe05ec8c9a1d0df7bdb0f562a10ced30e3e8f/duckdb-2.0.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e38daf0acbad9de7ce892d402b68409519291ee16af3c2cba85ace3b085c7eaa", size = 21655920, upload-time = "2026-10-21T12:51:04.36Z" },
174+ { url = "https://files.pythonhosted.org/packages/cc/33/01ad2535ee897db2bad70cbc800655167c53afd0b955eed84f0759c2f417/duckdb-2.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab51ec57d329d6f3df37c0287b55d71c3fc07bb54b61d1afed09104f953b1431", size = 24371005, upload-time = "2026-10-21T12:51:06.86Z" },
175+ { url = "https://files.pythonhosted.org/packages/09/d3/9e8ca1242b98f04041bef09686ecfe098b41f820aa23240f63f360b64eaf/duckdb-2.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:f61c96547ed432f54dae42a717fd88563eda55cb47549715fac9f61f770c205a", size = 15368860, upload-time = "2026-10-21T12:51:09.395Z" },
176+ { url = "https://files.pythonhosted.org/packages/37/81/6ea5dc1050f86ec4dd038a2774766b42247ba6104e7785f13a0c600e2b33/duckdb-2.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:c5e6708efaa8647e0914a16f1914354419e529a1925929950e7d610418e45873", size = 15726489, upload-time = "2026-10-21T12:51:11.643Z" },
177+]
178+
179+[[package]]
180+name = "execnet"
181+version = "2.1.2"
182+source = { registry = "https://pypi.org/simple" }
183+sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
184+wheels = [
185+ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
186+]
187+
188+[[package]]
189+name = "fonttools"
190+version = "4.68.0"
191+source = { registry = "https://pypi.org/simple" }
192+sdist = { url = "https://files.pythonhosted.org/packages/0c/b5/672312ae80ce8b8eb9779c56052a24c8f36652aa7293a1a420553bcf03e5/fonttools-4.68.0.tar.gz", hash = "sha256:73294e233070f71e2b3653b6bd8564af676d57f26d075761fb06d1ab2e4a7a5d", size = 3848583, upload-time = "2026-10-27T16:30:26.451Z" }
193+wheels = [
194+ { url = "https://files.pythonhosted.org/packages/15/63/f332b2099599b4911786a6f6fd69552fdd5ae6a45cfaffb0c881092f36c8/fonttools-4.68.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f265c85432c398d86f26add143423924a4747879b2dce001a3ffe0a5fed30638", size = 3143556, upload-time = "2026-10-27T16:28:21.812Z" },
195+ { url = "https://files.pythonhosted.org/packages/c8/97/68816bb56843e06631ff11517e4fb35fc62d76aec6e29f11df095e7f33c5/fonttools-4.68.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d225096687fc3bac0ebe3357e95b2dd8b0a49d191f016db0771d537ccdd6f47e", size = 2633689, upload-time = "2026-10-27T16:28:23.54Z" },
196+ { url = "https://files.pythonhosted.org/packages/21/94/b125c7a6d154d17f4f699150ef3abebd9d8ad0f7eddce9dad649a9e5c1d8/fonttools-4.68.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b41e71b2657c9843b2327b4e3c116db1757dc8cd05d978cd47cb5061272d4997", size = 5462605, upload-time = "2026-10-27T16:28:25.774Z" },
197+ { url = "https://files.pythonhosted.org/packages/7f/58/fe13db71e3f5a020c6875f349806e84f7550f3f24cb3cdbabdd693968b1d/fonttools-4.68.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3edce69ecdefa73ab1041901ca22eefed6ebb93a77bc77e889d940479f25bf7d", size = 5443448, upload-time = "2026-10-27T16:28:27.898Z" },
198+ { url = "https://files.pythonhosted.org/packages/f1/7f/48236f8c626007670586dd6b0d61ed80323a6b17ead763c881d717525cea/fonttools-4.68.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8d5674de2b614575f0751671727056af042ac1d61beacb59a298d74282fa492", size = 5399992, upload-time = "2026-10-27T16:28:29.99Z" },
199+ { url = "https://files.pythonhosted.org/packages/cb/0d/09e9a433430e277e454eace1c4d02c3691a33d5257bd234702a427a9d090/fonttools-4.68.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7ab8097e7e191784974c5608eb95fb31e1243fe3054cb66778ac82558e9b36f8", size = 5563739, upload-time = "2026-10-27T16:28:32.691Z" },
200+ { url = "https://files.pythonhosted.org/packages/ac/54/6708f4c6742de2d5bf4280d8bde22fb8aef5120328bdc19ca34a9d2e8e8b/fonttools-4.68.0-cp312-cp312-win32.whl", hash = "sha256:1695cd2e1f9768fe3f14d369ce6ef1b3e8561069cc9e47059571985d1ea42614", size = 2481614, upload-time = "2026-10-27T16:28:35.122Z" },
201+ { url = "https://files.pythonhosted.org/packages/1c/3e/ef786310a3774ae7aacaa288ca6d1e1dfd03c24214137478e4b630b76af0/fonttools-4.68.0-cp312-cp312-win_amd64.whl", hash = "sha256:fedc183bdbc3fdbfbdca26e0e04566d089cbb404712509a4e824608e9304cc77", size = 2533508, upload-time = "2026-10-27T16:28:37.085Z" },
202+ { url = "https://files.pythonhosted.org/packages/06/11/2cc4272ddd485c448c0363be9beb0a24d3f429f1cc8c5aafa716c508e899/fonttools-4.68.0-cp312-cp312-win_arm64.whl", hash = "sha256:95628bbc5ea81d5761268f4c4f1ba709af510b970f363498ac801c0c1fd7a4a8", size = 2502049, upload-time = "2026-10-27T16:28:39.355Z" },
203+ { url = "https://files.pythonhosted.org/packages/05/ae/e2d75b7f582c1cc018ab94b8bbfbad7ab501b340debfa256f48125cae3eb/fonttools-4.68.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f15de48b82a816c72f358e772792801bd3788a156caa76e1806cb0aa8148cd61", size = 3141370, upload-time = "2026-10-27T16:28:41.721Z" },
204+ { url = "https://files.pythonhosted.org/packages/fe/80/73d5297441b42d42b64d91cd4fffdff1541fac6171b0aca0a167c26d1c33/fonttools-4.68.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:86d319a1feedfc75c8ff582d947b21fa1854e2829cb8cf9428784d750a05f1d9", size = 2633412, upload-time = "2026-10-27T16:28:43.612Z" },
205+ { url = "https://files.pythonhosted.org/packages/36/29/16501aa69a586b8e8f810d7be19584aa026a0bca46c598ac516cfdd2a31a/fonttools-4.68.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04a4868dcfcc23f00bb033147d5afc1a118a992842644c3eb9da55467ab4fd74", size = 5435968, upload-time = "2026-10-27T16:28:45.651Z" },
206+ { url = "https://files.pythonhosted.org/packages/79/c6/4b44abbffa01f85d6d0f0b9325a712019b4e2e6208f999ac6846d8da1346/fonttools-4.68.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08851f7d2b983412f490b73a7c56f17cbda1d4d85d2ad3f857564bf15b5d9475", size = 5418193, upload-time = "2026-10-27T16:28:49.238Z" },
207+ { url = "https://files.pythonhosted.org/packages/98/0d/15cffdf13e156bb75ee421eee319fa8c5fb91a48a9a3e84685bf786df5be/fonttools-4.68.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6f7c5fabf5b1d9ca79f204664f2c3e2f8218e50aebc43e85e02df0b2d9cc313", size = 5378957, upload-time = "2026-10-27T16:28:51.889Z" },
208+ { url = "https://files.pythonhosted.org/packages/9f/18/174052f12611c1088b61eeaa4019a8f1aedbcb1e985c33ef8fbe0c1ba674/fonttools-4.68.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3ecdb0b248cd0bf43100a8b3cb40f7b45d52ea4be8616edea5dea8fefe90229", size = 5536796, upload-time = "2026-10-27T16:28:54.445Z" },
209+ { url = "https://files.pythonhosted.org/packages/35/d6/892213f45f63565f3f7eca3bce484a6f52ab7f7eda61851f93b1be829d5e/fonttools-4.68.0-cp313-cp313-win32.whl", hash = "sha256:4da99b83ae17f531838bc750abf56164ff37e1543e8e9b2b5403548f50497ce0", size = 2479972, upload-time = "2026-10-27T16:28:56.749Z" },
210+ { url = "https://files.pythonhosted.org/packages/61/2b/6c6f578176edeec83c5e02b7e745c1c8029f8cec4e32b4898d2c573bb881/fonttools-4.68.0-cp313-cp313-win_amd64.whl", hash = "sha256:6ed796b220654b4d37ad20af73419ec7969b669dd678cab88afb602c5683a331", size = 2531658, upload-time = "2026-10-27T16:28:59.013Z" },
211+ { url = "https://files.pythonhosted.org/packages/3c/b9/7cc71534bc954346451003bf5c9c117af835aea681a13a2847222ccf19b2/fonttools-4.68.0-cp313-cp313-win_arm64.whl", hash = "sha256:cd4b0d00523cae1e5e1eaed1d2b85ef342cec000f58d0738e12b064d72a7bbc2", size = 2499695, upload-time = "2026-10-27T16:29:01.511Z" },
212+ { url = "https://files.pythonhosted.org/packages/1a/70/ee6655887166a111d0c4d7ca61f03c90b141cdc6f70c85138fd0b8bfa2e3/fonttools-4.68.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b2ee7b9c1a5e1d637073c39b0978956febad7e862a1269719f527934c65535a4", size = 3145377, upload-time = "2026-10-27T16:29:03.757Z" },
213+ { url = "https://files.pythonhosted.org/packages/d6/37/66e7ab417660cb2c65889eb5ad8d80cc5a7cb2c09dd1becdbe6d56bac4fe/fonttools-4.68.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9d80813ae74b969ef2166a3040b591826e80ecd879bdeab7f13f4e8eaf08b345", size = 2634889, upload-time = "2026-10-27T16:29:05.651Z" },
214+ { url = "https://files.pythonhosted.org/packages/0c/ad/da985167af1988b82112cf133e826cf075cecb28d120778589265cb3125d/fonttools-4.68.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6837def5de49772b82683dadd836df214def04b307cb98d0e7723d49e7e15bb", size = 5420416, upload-time = "2026-10-27T16:29:07.502Z" },
215+ { url = "https://files.pythonhosted.org/packages/b7/67/e9f2eb2aed41d4ed251a3c6f66e104685f0a03d8ded7ed87f1f0fc63898c/fonttools-4.68.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cbb63b26d33acc3eb61f544c22a0cadbe01682fc064b266bb0e33fa66cea853f", size = 5363969, upload-time = "2026-10-27T16:29:10.172Z" },
216+ { url = "https://files.pythonhosted.org/packages/e6/06/c4510ac6b43b374b1b707b99fdd0ba1d4059d2fb516584a412c0396fedef/fonttools-4.68.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:81ea963d4390bb9e79f91a905310d91ac7ff7675d8e1c0cfc0f1712521c41a43", size = 5362373, upload-time = "2026-10-27T16:29:12.298Z" },
217+ { url = "https://files.pythonhosted.org/packages/e0/ef/ea0bf283a30a88a84a47078ff39f7bdefa0346a8bd7737439fd33084031f/fonttools-4.68.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3b9b87645bcdbfc3f30240e2758a37bbef4fdd0f708683b2bfceb60975d63e59", size = 5495553, upload-time = "2026-10-27T16:29:14.614Z" },
218+ { url = "https://files.pythonhosted.org/packages/95/c0/d493f8c8974859e57920ce193ffbe277423da7eebccbb9241bc31e1b5f1d/fonttools-4.68.0-cp314-cp314-win32.whl", hash = "sha256:39913c1f576af86c69519dc351302332561ee62b39fed04dcfa8019d9c2d129f", size = 2484057, upload-time = "2026-10-27T16:29:16.45Z" },
219+ { url = "https://files.pythonhosted.org/packages/96/41/c9ca629ffe1b883c1422540a8a11a4ea09616366ab585ab4ee9833f2cd75/fonttools-4.68.0-cp314-cp314-win_amd64.whl", hash = "sha256:1782f2cf952eb262c4126b1f57b5f0e027e71bf2b632350a033dec526f448428", size = 2536130, upload-time = "2026-10-27T16:29:18.399Z" },
220+ { url = "https://files.pythonhosted.org/packages/17/40/f6264c988076393db3e0b00d6d0ea2a53ce40d445e8a196be241d16d362c/fonttools-4.68.0-cp314-cp314-win_arm64.whl", hash = "sha256:f0b39924698a85bee107184a3c2ee9ae27248144977160534f7d558831db50cf", size = 2508386, upload-time = "2026-10-27T16:29:20.318Z" },
221+ { url = "https://files.pythonhosted.org/packages/c5/d8/a4d710362f3f23aecc16015eeed1d602187189af7f63a63546233ad2fa73/fonttools-4.68.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:956230858b7a7bb9f5e0971ae07dc64b6155307304acd8c62aedca30e6aae874", size = 3219047, upload-time = "2026-10-27T16:29:22.337Z" },
222+ { url = "https://files.pythonhosted.org/packages/4d/4d/0d516a1d3d34e84c56237ba1d8dfed8575c350b0e8d39aa30b518d089daf/fonttools-4.68.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b637d82a488116da2869afbddccfd93ca14e3c47b404c8ec5a9bad9d4381b08c", size = 2668060, upload-time = "2026-10-27T16:29:24.536Z" },
223+ { url = "https://files.pythonhosted.org/packages/4d/40/56893c685cb1ee7d7ee30d0ac94adf92020787cf5a8e15c2e4e6944f0bec/fonttools-4.68.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:419bf78474adaa6fb320a06822f4f941b19345d598a9de379d6e2f0e4d819df6", size = 5593080, upload-time = "2026-10-27T16:29:26.51Z" },
224+ { url = "https://files.pythonhosted.org/packages/41/b6/bc0bbad6f1aef22519c4e731c6dc8655e92c6f7204277ec36502ea7e76cd/fonttools-4.68.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe80d05dd85e6ac58d0cd4fa10ddb96252a6f1046ee0fd00cd1d48f0c25921ac", size = 5399669, upload-time = "2026-10-27T16:29:28.969Z" },
225+ { url = "https://files.pythonhosted.org/packages/c1/b4/4404a91dc699dd430732d366996cef6d8c8f61c758b5e544a76903db580a/fonttools-4.68.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cea1969b9bfe230cb3fd66f1c66da5a63a9a917d9cd25a0839524d3d1a88b40e", size = 5460446, upload-time = "2026-10-27T16:29:31.15Z" },
226+ { url = "https://files.pythonhosted.org/packages/56/09/66876a0d1a01f7c16eb9f7c8ebcf064ebbd6b249f837691cb5a35780316a/fonttools-4.68.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fde43b9785c7f105aa563b6b5481edabea4ab6be7d9ef14ff4d76225950923e8", size = 5495792, upload-time = "2026-10-27T16:29:33.455Z" },
227+ { url = "https://files.pythonhosted.org/packages/15/7d/1d702dc628e6f5b0d63be906025afc5e5b487cee48128943bb0a71d2bcb3/fonttools-4.68.0-cp314-cp314t-win32.whl", hash = "sha256:a325e6d7f96f20b3004658c5ed17ccfce5b66277b43ccd883035c61be6945275", size = 2517805, upload-time = "2026-10-27T16:29:36.929Z" },
228+ { url = "https://files.pythonhosted.org/packages/f3/42/8fa0741e08ae16be66a0d2e6d67f918fa0f2961bad70ccef84f9af0767cc/fonttools-4.68.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e80ac66627a01698675ce6e98d90ea9817d1e06644129ec8b0093f66ff1cbeac", size = 2567666, upload-time = "2026-10-27T16:29:40.081Z" },
229+ { url = "https://files.pythonhosted.org/packages/13/2f/b73cf496570e8d2c66a6099f825505283ef8e944bd588e0b63036d6b2412/fonttools-4.68.0-cp314-cp314t-win_arm64.whl", hash = "sha256:83bd3d558c15f4e1d87b8f3fcfb337c2bce63c7e5c05212abc8fdcf15e216c54", size = 2532338, upload-time = "2026-10-27T16:29:42.073Z" },
230+ { url = "https://files.pythonhosted.org/packages/24/af/dda3fbb1da1d3cf5bf8cd6a924ab799fd2a483382df7de6f9770b543ef4f/fonttools-4.68.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:ab6505dfbe3199453d8542b25f55c1ea3260d38029d9db58c0c7304d438abdc0", size = 3139761, upload-time = "2026-10-27T16:29:44.288Z" },
231+ { url = "https://files.pythonhosted.org/packages/4f/47/4294eececd4e7c38bcb98c78be96003b4bbfc459c30adec13104b83112c1/fonttools-4.68.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:03c9ef1272e464747f67c623f76449b888da6d5de7fb46275bb36488b9ceedba", size = 2633458, upload-time = "2026-10-27T16:29:46.202Z" },
232+ { url = "https://files.pythonhosted.org/packages/df/3d/3ee0940101d12a68cfd5e3d91380933878d99bd082eb9df348eb768b0375/fonttools-4.68.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5083a456d68d2995ab78600ccb18ec08c42f9bc14f0ef80c12e2e5b11cc371ce", size = 5417615, upload-time = "2026-10-27T16:29:48.404Z" },
233+ { url = "https://files.pythonhosted.org/packages/a1/ff/a97925464230d9c39c94e4cd7c5690db950373b34317e7f0ffe7b64b8d49/fonttools-4.68.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b65c2f3c4449383429507a7e1b372bd759f60a2df8ddc8d6c1cf9010585576b1", size = 5380964, upload-time = "2026-10-27T16:29:50.809Z" },
234+ { url = "https://files.pythonhosted.org/packages/ee/51/f467501a75bb383f1acbcc7a7d8ce196c02bea770e2bdc62d269c18a31a1/fonttools-4.68.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:039142b75a068c93f8596ef68787f82c74940b9153d24c224ab31e750eb083a2", size = 5357813, upload-time = "2026-10-27T16:29:52.892Z" },
235+ { url = "https://files.pythonhosted.org/packages/ba/1b/ebb9c32a19a89e8c5cfc63833d7edb70019d3184281452cbc1a24cadce57/fonttools-4.68.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1dde2f0ec3fd0fb43cf818593b38a8202eb7f3b59cae86289a8b19fbf6f6313b", size = 5505577, upload-time = "2026-10-27T16:29:54.95Z" },
236+ { url = "https://files.pythonhosted.org/packages/18/c3/080bf6342ed37c4304cf222cf80afd2a145cd8f37a758d812cda81ba3e12/fonttools-4.68.0-cp315-cp315-win32.whl", hash = "sha256:4bc352572abc800177fba513995c01b1cfbe8c810ac2506cbf29f0795ae061c5", size = 2482863, upload-time = "2026-10-27T16:29:57.029Z" },
237+ { url = "https://files.pythonhosted.org/packages/f2/b7/8ce34a43072ac7b1a937c5a0ee8e024db5d774bdf07708bb7593f3e5540d/fonttools-4.68.0-cp315-cp315-win_amd64.whl", hash = "sha256:9b6e4ae9763c67df2b0f4c4f1a0b18e47ee6572eb602b82e798acc88b64a6137", size = 2535110, upload-time = "2026-10-27T16:29:58.999Z" },
238+ { url = "https://files.pythonhosted.org/packages/6d/35/d58d4ac6dd3de099c11c42486fc4a51b1ccb2c1c3259b19956e9244d5623/fonttools-4.68.0-cp315-cp315-win_arm64.whl", hash = "sha256:e00bfc6fc0158f156d34f923829e6f62ca00df2e0ea42ba3b0db4089b6d921c2", size = 2507240, upload-time = "2026-10-27T16:30:01.439Z" },
239+ { url = "https://files.pythonhosted.org/packages/29/16/034e1eee6264cd14ef4404f70c299a9b0570d83aca24a65ddf3a16ab3b42/fonttools-4.68.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0841a4dac69efda309fca2ed0834186ee66d39787f1d26430ed7f779a699a096", size = 3210579, upload-time = "2026-10-27T16:30:03.601Z" },
240+ { url = "https://files.pythonhosted.org/packages/17/37/626504f427aa92d1d872336eab454ad71b6bc079063cc1f4af314995a48e/fonttools-4.68.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:93afd51f30ae38f7d826f4632eb799a06bd94e1fb4bc59fdce6d291e93491027", size = 2664302, upload-time = "2026-10-27T16:30:05.639Z" },
241+ { url = "https://files.pythonhosted.org/packages/62/13/bc455cb7f8e467ed4ea1146c1ecad76d6c78102031d937cae71efc176c42/fonttools-4.68.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82b8024fbdcddc399b1e21575b9b82f46b8391e711a44d6bd4a1616b0aca3681", size = 5570974, upload-time = "2026-10-27T16:30:07.457Z" },
242+ { url = "https://files.pythonhosted.org/packages/23/06/33ce5c7d3bb59dda92fbf669053d5fdaac2e0d566390fdf20746ee83f39f/fonttools-4.68.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fee5880486c3fd6f45f5a1679a9f4291ae74e503d725de7f1e30efa2d627440", size = 5392631, upload-time = "2026-10-27T16:30:09.587Z" },
243+ { url = "https://files.pythonhosted.org/packages/3c/c0/414f221a7da04ddb6d30a8171ecdba36e2ab4351c5c62ec319621ee76494/fonttools-4.68.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:4be32afa53ba3dea243b95b62e9b2e6ed6cf7096acd741d379c12f4113694ddc", size = 5438942, upload-time = "2026-10-27T16:30:11.966Z" },
244+ { url = "https://files.pythonhosted.org/packages/98/71/797da0cb0fdb3839b4aa65d30854b850832c74dc14ce0ba5bfe812c6a008/fonttools-4.68.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6e73d4b1526343b45bbafa9badc0396a49d902f9a1774666ed4244f1df717629", size = 5485323, upload-time = "2026-10-27T16:30:15.074Z" },
245+ { url = "https://files.pythonhosted.org/packages/c1/47/a6989b4de0f427817f2e436f69664acc3aa9600d3df5f0eb3444cd073f14/fonttools-4.68.0-cp315-cp315t-win32.whl", hash = "sha256:573ccce8b5fd9b6952dcbc5d896255041ab330cc33520d13b1266fca859d8aba", size = 2514943, upload-time = "2026-10-27T16:30:17.326Z" },
246+ { url = "https://files.pythonhosted.org/packages/5e/38/7311ade45b67dc2c2a4f9d1af4c18285f92591235e8c6cab4f9cecd36516/fonttools-4.68.0-cp315-cp315t-win_amd64.whl", hash = "sha256:6aea5cdbc32f5fd202cf8dec053a784b08692f8b1992af80eba32bef82bf84c7", size = 2563334, upload-time = "2026-10-27T16:30:19.883Z" },
247+ { url = "https://files.pythonhosted.org/packages/8a/ee/fd08713d15bf0f6487a72e415e91ef4a6b52ca96fa8f879533107a41e18d/fonttools-4.68.0-cp315-cp315t-win_arm64.whl", hash = "sha256:b612613f0165f560571c4e372af0ebbb1c4b36067de409922dcea349beb8bb20", size = 2528487, upload-time = "2026-10-27T16:30:22.222Z" },
248+ { url = "https://files.pythonhosted.org/packages/35/74/4ea331ecc65ba05a58b1e4e8ccd604c8a15565f876dfbf93fc2a2ea2e22b/fonttools-4.68.0-py3-none-any.whl", hash = "sha256:d76f8fdcb6936f5e1045a0c9dd721a4bf11112e60d1e243bfa1d39f06a8b3104", size = 1248357, upload-time = "2026-10-27T16:30:24.389Z" },
249+]
250+
251+[[package]]
252+name = "formulaic"
253+version = "1.2.2"
254+source = { registry = "https://pypi.org/simple" }
255+dependencies = [
256+ { name = "interface-meta" },
257+ { name = "narwhals" },
258+ { name = "numpy" },
259+ { name = "pandas" },
260+ { name = "scipy" },
261+ { name = "typing-extensions" },
262+ { name = "wrapt" },
263+]
264+sdist = { url = "https://files.pythonhosted.org/packages/f1/87/e7b9b39d218793f159d7425e264c0ac196da07f1decc333001502fac02fb/formulaic-1.2.2.tar.gz", hash = "sha256:c99e8f11ff7d327eaecaf63855ca69b7fa0da100ad6c0041ef80912fbac667e6", size = 657522, upload-time = "2026-06-02T18:24:42.632Z" }
265+wheels = [
266+ { url = "https://files.pythonhosted.org/packages/fc/75/0576b03f7889ad25b5385b4f6c69e0543713425a6f193b056c9b0a2e65ce/formulaic-1.2.2-py3-none-any.whl", hash = "sha256:0f84ff49e3fc9dc0e68ab08a0a9427874021aa6c558e66b44dc634a35739b09b", size = 118939, upload-time = "2026-06-02T18:24:41.344Z" },
267+]
268+
269+[[package]]
270+name = "h11"
271+version = "0.16.0"
272+source = { registry = "https://pypi.org/simple" }
273+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
274+wheels = [
275+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
276+]
277+
278+[[package]]
279+name = "httpcore2"
280+version = "2.16.0"
281+source = { registry = "https://pypi.org/simple" }
282+dependencies = [
283+ { name = "h11", marker = "sys_platform != 'emscripten'" },
284+ { name = "truststore", marker = "sys_platform != 'emscripten'" },
285+]
286+sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" }
287+wheels = [
288+ { url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" },
289+]
290+
291+[[package]]
292+name = "httpx2"
293+version = "2.16.0"
294+source = { registry = "https://pypi.org/simple" }
295+dependencies = [
296+ { name = "anyio", marker = "sys_platform != 'emscripten'" },
297+ { name = "httpcore2", marker = "sys_platform != 'emscripten'" },
298+ { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" },
299+ { name = "idna" },
300+ { name = "truststore", marker = "sys_platform != 'emscripten'" },
301+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
302+]
303+sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" }
304+wheels = [
305+ { url = "https://files.pythonhosted.org/packages/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" },
306+]
307+
308+[[package]]
309+name = "httpx2-jsfetch"
310+version = "1.0"
311+source = { registry = "https://pypi.org/simple" }
312+sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" }
313+wheels = [
314+ { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
315+]
316+
317+[[package]]
318+name = "hypothesis"
319+version = "6.170.12"
320+source = { registry = "https://pypi.org/simple" }
321+dependencies = [
322+ { name = "sortedcontainers" },
323+]
324+sdist = { url = "https://files.pythonhosted.org/packages/11/02/d55560240dcb9f3e758213f0079b312271a6e832c5f84e767f7af6130898/hypothesis-6.170.12.tar.gz", hash = "sha256:92b353d21a010b50b638c730ce67cb631ca78a369b146bcc28b3488f83136e35", size = 521506, upload-time = "2026-10-26T17:12:08.305Z" }
325+wheels = [
326+ { url = "https://files.pythonhosted.org/packages/eb/08/e460fc3c5fcbd8e7ceecd45a9503fdcc8780c3a74b12354bb239f023068e/hypothesis-6.170.12-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8d601516e32bd822c5558b553d186e35cd1451d0a241403a2357aee0d8085278", size = 800088, upload-time = "2026-10-26T17:10:30.787Z" },
327+ { url = "https://files.pythonhosted.org/packages/d4/c4/60086043aa1688eeaff8236ca131bad1c6bd1b651806204efdf2f910d737/hypothesis-6.170.12-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b9129107c3d13cb778cb4ee438358677c9e20e916a824073fc601649a28f78fb", size = 795251, upload-time = "2026-10-26T17:10:32.15Z" },
328+ { url = "https://files.pythonhosted.org/packages/e7/f8/4211426f3d9b3a4315441792ef784109db87107a7670fc40954ec229a3d6/hypothesis-6.170.12-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13a922a8498457d293f20b3682bca09eb27495d8b83d1620301a1331a90b97e1", size = 1124178, upload-time = "2026-10-26T17:10:27.046Z" },
329+ { url = "https://files.pythonhosted.org/packages/b6/a5/1ee9e9eac46a7bc7f6b0bcd516e64be4bd8ff2c9b59b7a7bb5dd666e6217/hypothesis-6.170.12-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcb5a1fe6c0d0a1167ea6683e1adb4794b3ce3d9425f6f2dd8ee1341e6851398", size = 1154653, upload-time = "2026-10-26T17:11:00.946Z" },
330+ { url = "https://files.pythonhosted.org/packages/32/1a/fd539478f430cd096290bfa5f7ed714a7034bea82761fc3cb45448847466/hypothesis-6.170.12-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a052212ada966f9d770b402a6f07c2f552e503242d8c186ba10f1de81cc79c6", size = 1148554, upload-time = "2026-10-26T17:10:24.438Z" },
331+ { url = "https://files.pythonhosted.org/packages/e0/c4/a5c0a87ba22d96cfa1a4f716f3485d2cb247a96fbfa6ebd042f2b87df771/hypothesis-6.170.12-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f59d0530d25b96fbdcabf6d2eb10d740e001dd01ff37729b0bbcaedf67ea298", size = 1201308, upload-time = "2026-10-26T17:10:42.748Z" },
332+ { url = "https://files.pythonhosted.org/packages/12/81/4012aa8f70ce549ac1503078e196175527584e0c86197f625a7c5a7eb7a9/hypothesis-6.170.12-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f58c29512520a29c4fe90545f7f299cf7b42f018b9218e1c1ee1a3045df5bd", size = 1167564, upload-time = "2026-10-26T17:10:22.516Z" },
333+ { url = "https://files.pythonhosted.org/packages/ad/e2/72fe8caf0c5833d2546078cbef2e3f45d9f73e8ce5f38a9ef8fb424ba5da/hypothesis-6.170.12-cp311-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:abde1babc40ecfdbd5ec7837fd47ff0799df27190ff2f097d88c4fa802741068", size = 1123683, upload-time = "2026-10-26T17:10:25.77Z" },
334+ { url = "https://files.pythonhosted.org/packages/aa/f8/222034c030457a28caf4ca52264088ff8b547f5c2bbaca2c26da1dafdd72/hypothesis-6.170.12-cp311-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d63321e5ec4b3c1b34298e2b44fe79bc607d230d4799a72d4a083856843e6b8c", size = 1161998, upload-time = "2026-10-26T17:11:55.968Z" },
335+ { url = "https://files.pythonhosted.org/packages/25/91/3eb747c2334c2f89cb5c7385d490c25602389e7ab19e2bc2b57940c75d11/hypothesis-6.170.12-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:98af0af451ed0c7ec21640490a5454ff57ac67ad5381db489effb2305c3c3165", size = 1299097, upload-time = "2026-10-26T17:11:20.238Z" },
336+ { url = "https://files.pythonhosted.org/packages/83/2c/c7104b8b3d0451be15a4fd4f04043fb0a73092a5f042b3fbe73e25a8bc14/hypothesis-6.170.12-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0992ada76db76aace71f4514e66730f6c836b507c57f821e36b474f2a10fe7ab", size = 1427748, upload-time = "2026-10-26T17:11:25.643Z" },
337+ { url = "https://files.pythonhosted.org/packages/a7/c9/aa3538690ec226b66e8e4a22ce714aa8cce67da6a9496209654d22a3db18/hypothesis-6.170.12-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:64a3b3faa02238e08d5dfe66ef8891571a511d4a92e5f45dbd41c6c277975689", size = 1381824, upload-time = "2026-10-26T17:10:46.797Z" },
338+ { url = "https://files.pythonhosted.org/packages/61/2d/ac7320c4f0b34fdd065e179f932aae6b0420b1811fb0df95e28511fdd5a0/hypothesis-6.170.12-cp311-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:0baceceb4a32310186c1cefacb28874f6298658f9486f7e90ca5f366fb9d3627", size = 1277780, upload-time = "2026-10-26T17:11:06.139Z" },
339+ { url = "https://files.pythonhosted.org/packages/fd/7b/c1aa4b06db40e8635d2444f43997a7c1d0e0f2dfc31264d590b1fb76d994/hypothesis-6.170.12-cp311-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:e6ddbab42945eb706ad0c3c7ae05d8df1f1b63ca62e212959169ab8baf29bc9b", size = 1293539, upload-time = "2026-10-26T17:11:54.406Z" },
340+ { url = "https://files.pythonhosted.org/packages/15/3e/68e83242d21561aff4a0f1dbc7edc1341ec397163f2a7d5481e26f6465a6/hypothesis-6.170.12-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f240b0f4a5c8d0b0399b854e76351d266d73692cc55c227f23ac2680b82891c2", size = 1333281, upload-time = "2026-10-26T17:12:01.444Z" },
341+ { url = "https://files.pythonhosted.org/packages/89/ac/0a024b48507e6db4378fc51b8f5ed9a5a0d2fe874a2de91cb44328e19802/hypothesis-6.170.12-cp311-abi3-win32.whl", hash = "sha256:cf96091375fa2e612cece3926b1cfc72763b6bd00c9a2052ee839d4cd7d3ef85", size = 687286, upload-time = "2026-10-26T17:11:46.263Z" },
342+ { url = "https://files.pythonhosted.org/packages/df/0d/cdf07a645b2fe1224e70a1080670fc793b296333af3f0caf73409729f294/hypothesis-6.170.12-cp311-abi3-win_amd64.whl", hash = "sha256:41e47d6c82e69138de731e39467d244a42e752bfa3d9462159e4c00d414fe7df", size = 693348, upload-time = "2026-10-26T17:11:38.561Z" },
343+ { url = "https://files.pythonhosted.org/packages/22/07/d5ff2b49491c287dca5c8c75537b4cdc901fe550970e9771996a7b9d9048/hypothesis-6.170.12-cp311-abi3-win_arm64.whl", hash = "sha256:c564cc9e1e1a5ca833e1f6620371e926f61817ed7d466cde9e190a029ae3eeaf", size = 691653, upload-time = "2026-10-26T17:10:50.59Z" },
344+ { url = "https://files.pythonhosted.org/packages/da/f6/424ecd49ba7339329e937ea3f88860ca7a455797a7184886ccaf73b7269b/hypothesis-6.170.12-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0d010e40387d8c3a5e2c8fde71459ce6f2bb5e8bba8914731780529b79a0b5b0", size = 801881, upload-time = "2026-10-26T17:11:59.754Z" },
345+ { url = "https://files.pythonhosted.org/packages/e2/05/e38e2de5486b03670360d47e1deb419917754fc9daf2aaee3ad185083d5a/hypothesis-6.170.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94b05dd80631d60f672c7d87d4280be9efa31b3116d0d88e71b7d26951fd3f94", size = 793880, upload-time = "2026-10-26T17:12:03.87Z" },
346+ { url = "https://files.pythonhosted.org/packages/af/71/4d693faeabc67d9d000db253e092ef72dd5725aaca3fd897a0f6a484acf9/hypothesis-6.170.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e674b6a7a7f2dee9bdb1da9b42c78edd971fea6758640997a9d8bb32cfddeb9", size = 1123538, upload-time = "2026-10-26T17:11:31.265Z" },
347+ { url = "https://files.pythonhosted.org/packages/70/ff/4f1eb1bb67283240aac366b77e2ebbf864e1fd4d93c3762399f499d200db/hypothesis-6.170.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51ccd8964291b6cba57709b6acf1cad2e9c4347e993198ef8ece5653cdafcbdc", size = 1167229, upload-time = "2026-10-26T17:11:50.062Z" },
348+ { url = "https://files.pythonhosted.org/packages/cf/19/4e40637c37285231e4cbd6291fbce5cf9da325b6c73af53d9004f7030155/hypothesis-6.170.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:19fd1565947111aa1bba31689585f5501eca4f660762069d7515e5e1d3a45b3b", size = 1298417, upload-time = "2026-10-26T17:10:49.337Z" },
349+ { url = "https://files.pythonhosted.org/packages/57/96/97aa8d7115a399e409c24b3f7642477de3521da797e078ccb94d64a128b4/hypothesis-6.170.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5007cb77462b0e1ae444e05684fe799906673942ab18255ad6144ba2f9b74bfb", size = 1332856, upload-time = "2026-10-26T17:10:40.414Z" },
350+ { url = "https://files.pythonhosted.org/packages/96/ba/11c464d8aed7777b99eec35d31c474ea64caaa68e4da342609aa1bad2868/hypothesis-6.170.12-cp312-cp312-win_amd64.whl", hash = "sha256:51c7553a5d5a368347dc2f063ac1bc6ad5b3385b883ca9e7009bbeb52e79eaa6", size = 690967, upload-time = "2026-10-26T17:12:04.926Z" },
351+ { url = "https://files.pythonhosted.org/packages/11/f0/283fcf6c9ead403928a24cd3d32098bfcaf154d8556411cb2822120cc001/hypothesis-6.170.12-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:492eb0d70d8bda18322a1dee3e503796997bcab7415afea01b111551f7591e9c", size = 801848, upload-time = "2026-10-26T17:11:57.215Z" },
352+ { url = "https://files.pythonhosted.org/packages/79/c5/9df47092d6e7fa638f40ad268eb87476eec95897db98f3c6d11cc9680a65/hypothesis-6.170.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f67a87338893d5c050372ab3a1e818e6c5bd267f302624b7c444c18657b7da39", size = 793875, upload-time = "2026-10-26T17:10:48.069Z" },
353+ { url = "https://files.pythonhosted.org/packages/4a/76/79b52b312af5f2d9758293324078eba186a3f082ecba2879d606087e998f/hypothesis-6.170.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cae68b4bcae8c748ac16c91a39b2e13643400aa1b1cccc1682ec7ffdc9c82a6f", size = 1123439, upload-time = "2026-10-26T17:11:40.937Z" },
354+ { url = "https://files.pythonhosted.org/packages/05/b1/1e41fc27605bf9d7c43a8e873745e2eeafea80985cf517b1fef1e624fd07/hypothesis-6.170.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00275564eab789af6dba53947194eeff0937ef9cd82866cd49b1160f604b6b58", size = 1167165, upload-time = "2026-10-26T17:11:43.866Z" },
355+ { url = "https://files.pythonhosted.org/packages/05/ee/99cf7e7b034ab806041c9f3ad506ee7cbb83381284684d2406da42812918/hypothesis-6.170.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea6334c3d67021c0778d9db519249d58a6d1e07baebb42144d7f0b88d4db8cf8", size = 1298395, upload-time = "2026-10-26T17:10:45.339Z" },
356+ { url = "https://files.pythonhosted.org/packages/7b/20/94d3ec0b039f6f42265fa64b97145065e1e286dcab7e68c56581ac9852ac/hypothesis-6.170.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b8a29ec47aca3e3e7ad47ffca197368afbeed156f4c03db482213ff084b10a50", size = 1332887, upload-time = "2026-10-26T17:11:13.819Z" },
357+ { url = "https://files.pythonhosted.org/packages/06/26/e08bc6f179a542e69c36bd02d78a01edf2a2ee561e6f12b00ca038d29c0d/hypothesis-6.170.12-cp313-cp313-win_amd64.whl", hash = "sha256:b3c4fbf33cf0c96381a9f301c7f423d21164222411f56c2bb050a7e44fb6dc51", size = 690938, upload-time = "2026-10-26T17:11:36.285Z" },
358+ { url = "https://files.pythonhosted.org/packages/de/8a/bebaec693c114e312382f12e6d79efadb4a378044ceba67e0ac3ff962558/hypothesis-6.170.12-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:90a63aebc766fc574276519d2840b7c144768be2f15f409458567f652f206559", size = 802107, upload-time = "2026-10-26T17:11:24.406Z" },
359+ { url = "https://files.pythonhosted.org/packages/00/14/e988ad36da4df17b8ec9131f29f77fa2a1a385be06b0d7cd2a7c20a39d16/hypothesis-6.170.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1982c1660cc215679f34580690cd440c878208544e4eaf697c2f04698767aab", size = 793991, upload-time = "2026-10-26T17:11:45.133Z" },
360+ { url = "https://files.pythonhosted.org/packages/c5/5c/89593bb4c43d09a899878ba20ed00fb0e27896146e0b511d2449a045246a/hypothesis-6.170.12-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5648bad764aab81f38dc8fb9b80942e5e325e19c7d7497312e9f7ef63540d48f", size = 1123961, upload-time = "2026-10-26T17:12:07.238Z" },
361+ { url = "https://files.pythonhosted.org/packages/2d/ed/4a6cec0a929079b3c2103dbc71d5846c74264453cca30b2d0da61ca83e6a/hypothesis-6.170.12-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cb0a1042041ad3b03159c658214eb19d19ec9d0efcf03a84f4ed94de54f1630", size = 1167355, upload-time = "2026-10-26T17:11:27.146Z" },
362+ { url = "https://files.pythonhosted.org/packages/3b/86/e991b1e6cc88a0d2e648864819360c48d03769c31ceec2b2095970a63f7c/hypothesis-6.170.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:45eefe8622916a10bd5ac1639adde0552985896624eadf9d9fa807b5eaa60aaf", size = 1298917, upload-time = "2026-10-26T17:11:08.811Z" },
363+ { url = "https://files.pythonhosted.org/packages/e2/b4/a8c292a80eabfbf5201c0360a677176880f993e91c890db7582e579efeb1/hypothesis-6.170.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:503a07eb31b806c29d4731c5e9c842df74dfb184f183f82f682c19f658761a7a", size = 1333081, upload-time = "2026-10-26T17:10:56.94Z" },
364+ { url = "https://files.pythonhosted.org/packages/7f/52/434322320807b9585f3eb7e414e0bd92126548de0dbeb0dee0dd5f38b513/hypothesis-6.170.12-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a8ac6f5e5a943fcac211e09e858aaa90033e10e44295977a32fdc3cd4f180245", size = 633703, upload-time = "2026-10-26T17:12:02.785Z" },
365+ { url = "https://files.pythonhosted.org/packages/4d/d2/4a2483c029adf6bdb78b237c5cbedf411df97e31fbbb81c8bf4c3ffe6769/hypothesis-6.170.12-cp314-cp314-win_amd64.whl", hash = "sha256:f5d839f0ba43101b479f1559c3b3c2870fa0eb92ad86bca82f247fd3a9d98866", size = 690674, upload-time = "2026-10-26T17:10:39.303Z" },
366+ { url = "https://files.pythonhosted.org/packages/80/68/f7357b87c6e0b6f9d044f8247d8532cf79c68c3692f64800e3833261b9c7/hypothesis-6.170.12-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ee71dadea93346799b6f6825b62c75f67ddaae36ecfd96591fd1fff17f687f7b", size = 800195, upload-time = "2026-10-26T17:11:48.676Z" },
367+ { url = "https://files.pythonhosted.org/packages/a4/46/1c054440c5f2d562ae973db1d4ac6ece2a2aadf442b3887c0bba4716a57b/hypothesis-6.170.12-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:681dfd2e47d2a473a36b3075bd15126b476a40a4cd5a2e134c71f3fafb651628", size = 792428, upload-time = "2026-10-26T17:10:59.572Z" },
368+ { url = "https://files.pythonhosted.org/packages/20/8a/6ea8efa8fcef75f44a250bd121dfd49c6cbd4f12407d996d472ae6754bf0/hypothesis-6.170.12-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87d50be85d98a05f177941fb21c2619279a0c6a289b139fe510126580c9ffa91", size = 1122389, upload-time = "2026-10-26T17:11:02.296Z" },
369+ { url = "https://files.pythonhosted.org/packages/33/ae/62be30dbd660e288f028da31fc8a8809a1c2230ec589a96a04f6c2303a05/hypothesis-6.170.12-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb59d1a301e5ceb4b402026880c74ba1b65ba1c249dc169324681e6a74fb0d59", size = 1166268, upload-time = "2026-10-26T17:11:28.494Z" },
370+ { url = "https://files.pythonhosted.org/packages/00/8d/1aa266dbf967081bfcbea3ffad90f77f879e1294585bdc94b38ac56b7cbc/hypothesis-6.170.12-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d015bfcd7d317ab8752b05c4469278cda807be8aab4f2dbdbf6662df0fb6570", size = 1297161, upload-time = "2026-10-26T17:11:12.544Z" },
371+ { url = "https://files.pythonhosted.org/packages/a8/35/4626673a9a6ba0781e36a064ec03083c397702737db06dfb1f48af42a9a8/hypothesis-6.170.12-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3a3471e28d40532099751e1be61526f8a36329c4f3eb67a132526c795ff717f9", size = 1332087, upload-time = "2026-10-26T17:11:35.055Z" },
372+ { url = "https://files.pythonhosted.org/packages/6f/71/eb1725a0159c0759ba33e08964c3ce61335e304811e2dd4f0935cfd3dedf/hypothesis-6.170.12-cp314-cp314t-win_amd64.whl", hash = "sha256:322548a87c3bb790854f8688ad2e8a652f696305ee2a182c85a3e0bf4a7b0498", size = 690804, upload-time = "2026-10-26T17:11:21.424Z" },
373+ { url = "https://files.pythonhosted.org/packages/64/cc/d9f44015a7b402f965ca80dcd4d3d5c179535235617baf1e306d5cea1b47/hypothesis-6.170.12-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:9480e495c9bce6019971b5d70dbb737e3ba56d90c32d22930556c4c56bd6c528", size = 799824, upload-time = "2026-10-26T17:12:06.032Z" },
374+ { url = "https://files.pythonhosted.org/packages/cb/3e/5deca2e25b89e0ea4c8ede15a6e3ddf1c938671270c14add4a2491966fec/hypothesis-6.170.12-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:393f82b7f76141dac5082edba58287a7ee6cc255f986b7dd9dd5fc4c2bb12afa", size = 792273, upload-time = "2026-10-26T17:11:04.799Z" },
375+ { url = "https://files.pythonhosted.org/packages/57/be/9ec46e023726a54961af06521ebe6caeccfa9cd155e62feaf8cdc180286e/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84e49b1db4f3fce90e5e8fba7a4e81961ebe522c5db8b60e3fe5fcd58512813", size = 1121164, upload-time = "2026-10-26T17:10:35.868Z" },
376+ { url = "https://files.pythonhosted.org/packages/8f/52/f42a0ae67c2d05e39f6170f7487f00650cab495ab604792cad7184ab5ff3/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b9ac42c8e163c1da3938364f8338391f71e7c721b79ae5cb9b310c0bdb646f1", size = 1152061, upload-time = "2026-10-26T17:11:22.592Z" },
377+ { url = "https://files.pythonhosted.org/packages/78/a7/4699066529d2c70ffa2e4d9a8371363922a244fc6fcc29c0740075e62971/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d1a12d62292568f4b37afd7ca0e05fc80e11e896e989d5f6ce7dad44efaff6d", size = 1145781, upload-time = "2026-10-26T17:11:37.362Z" },
378+ { url = "https://files.pythonhosted.org/packages/43/ab/1588dce65e5343364c0a5e4b0d4924aa415c446e65a6cb0a4e900bc081eb/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f14d172dab9bb05394d5409699c3817f3f759ca0313ee2a8933b7c190169c2df", size = 1199556, upload-time = "2026-10-26T17:11:03.52Z" },
379+ { url = "https://files.pythonhosted.org/packages/1e/d5/11d5810838ffa5ee40c4c1fdb4e00de9fa8ca7037f9e8d52ea5de739d2f4/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb2a0cd5e3808077143290344646e6ed9e46fdf0c3777910f90763884de719e8", size = 1165475, upload-time = "2026-10-26T17:10:28.172Z" },
380+ { url = "https://files.pythonhosted.org/packages/bf/d6/751bb29605db0457bbdd2d497dbeb92874e348731ce4b7677ee8def67bc9/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:9db4f9d90a41e15938bbefd0b32dc93c589510ad1309f164e9436c11c1ad2ad0", size = 1119771, upload-time = "2026-10-26T17:10:29.561Z" },
381+ { url = "https://files.pythonhosted.org/packages/ea/25/c1eed293fb064ac3a128cf0b827199ba0487302fcd2f7dda73d692b4b757/hypothesis-6.170.12-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:10869687863bb79028025e5a4acf5d30d2da8b6d215f46f02c12d91647f14849", size = 1158253, upload-time = "2026-10-26T17:10:34.426Z" },
382+ { url = "https://files.pythonhosted.org/packages/26/5b/746b02f3b91bc11b77f7791a2da115afda4264e3ed8538a778abb2684d94/hypothesis-6.170.12-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:44463725ee0812b8226410294c8db69956ec726f22b42d133f5366fefa939cf9", size = 1295881, upload-time = "2026-10-26T17:10:33.242Z" },
383+ { url = "https://files.pythonhosted.org/packages/f0/75/d39b8caeb0f44e8e4e2cbda67dab372a5dded9cfc91daf8a7436633d1982/hypothesis-6.170.12-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:0d469c9cced28f7d7ed411dc41609b91ac19149947f7a68ac8404d576c433e85", size = 1425052, upload-time = "2026-10-26T17:11:39.715Z" },
384+ { url = "https://files.pythonhosted.org/packages/81/db/393ca7953415fbb9775824c7f8206e0ca004d664dddb2c5cdeea79de7e0c/hypothesis-6.170.12-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:02857858c8985fdd5652b56d4a5795e66416c80cb3dd5ea1e1d0ee496fc58cdd", size = 1378674, upload-time = "2026-10-26T17:11:11.101Z" },
385+ { url = "https://files.pythonhosted.org/packages/72/0b/7fe8b2f09c615886e454e418e0296d6b2756abd8866a83fdbcd8f912b9b3/hypothesis-6.170.12-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:b5db8c5e15e3e0e3135d41d9b1c3ae361823d65d0b19adf2bb8cb09b21108607", size = 1274395, upload-time = "2026-10-26T17:11:15.151Z" },
386+ { url = "https://files.pythonhosted.org/packages/3d/21/fa36acad45a3a9b1da13c3e1eb9d11202dd483e665f5ae5722d42702f32a/hypothesis-6.170.12-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:67ef145399463542418c8c47d8f0ec31458ba07c86289bcd008e5081f5e2213d", size = 1291512, upload-time = "2026-10-26T17:11:07.508Z" },
387+ { url = "https://files.pythonhosted.org/packages/d7/fd/2bb225a5196fcfae0c474f5c405e5772011c6601d6ed0eddffa2a3ae6faa/hypothesis-6.170.12-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d77f02bd604b764398acb493203dc2f45b7634cafa3bdd6b18d502ed3a1bfe0", size = 1331449, upload-time = "2026-10-26T17:11:17.753Z" },
388+ { url = "https://files.pythonhosted.org/packages/ae/72/01d3b1834116952263a01e8da4b1634f83ac0b38904c805f8f732fef553b/hypothesis-6.170.12-cp315-abi3.abi3t-win32.whl", hash = "sha256:2484674ee787451b050dd9bf80105786cb34899c1e5a75195f2c8feaad87c47d", size = 684613, upload-time = "2026-10-26T17:11:52.637Z" },
389+ { url = "https://files.pythonhosted.org/packages/14/54/fecb01f6b5d7adf58b104fe2d8abfebd5d51a7afda702d20cc55c767a3e8/hypothesis-6.170.12-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:020c2be4b16a09787b2d5b532ad1a75e20a4443c8b220e59a981c45d488fa4d8", size = 690578, upload-time = "2026-10-26T17:11:47.512Z" },
390+ { url = "https://files.pythonhosted.org/packages/0c/ac/cf60b7e7901412b4d60e26be9373f9b9b9e4fdf5ffdd739aa3e43fdea4d9/hypothesis-6.170.12-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:a39811c29cac6bb35ef775db6a0e276578350e4768ea9a6669f70dfcc3626f6e", size = 688566, upload-time = "2026-10-26T17:11:09.934Z" },
391+ { url = "https://files.pythonhosted.org/packages/45/fd/712d04bffaf9614ddaf9aef4ef355560dca122fe3408f668a1a929e42a2c/hypothesis-6.170.12-pp312-pypy312_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8414611abd203b0fe3d11fb7877108c775416d454e29d3b0d72b5f8968549237", size = 800956, upload-time = "2026-10-26T17:11:16.549Z" },
392+ { url = "https://files.pythonhosted.org/packages/93/7c/568a6a7ce8c6a140def54fc5185eb070de946d16518a57f51bc4338f2b9a/hypothesis-6.170.12-pp312-pypy312_pp73-macosx_11_0_arm64.whl", hash = "sha256:8cd063d3620c4097505f4e697fcaded527a9565333ae3e4ef6a9b727f47b67e4", size = 792848, upload-time = "2026-10-26T17:10:55.658Z" },
393+ { url = "https://files.pythonhosted.org/packages/f0/3e/4fafe1a50b0df2d032b6142e13366a7d37ed0cab5d6af749c430fb538f0a/hypothesis-6.170.12-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18ea9df1deec60fad073305438f050365a2ab8f5c7a577b5c8f96e5aaec620ad", size = 1122799, upload-time = "2026-10-26T17:11:19.011Z" },
394+ { url = "https://files.pythonhosted.org/packages/2f/24/b9b30baa2b6c3703162f8a5f49235d74df568ed941680edde70332e4fe42/hypothesis-6.170.12-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99efd12654a380a6d683b962f481969d08beb55bd2426424cd5178d3f9f12b23", size = 1167006, upload-time = "2026-10-26T17:11:29.968Z" },
395+ { url = "https://files.pythonhosted.org/packages/ce/13/dcfda3cbfcbd0501aefe0ee6e46591671a19df3ba18d65fdfd4229c5e3b6/hypothesis-6.170.12-pp312-pypy312_pp73-win_amd64.whl", hash = "sha256:8259bf11eed5379c8bc69c7fd998baa0e122350ab3e5b3432d5ef2e22e11d8e3", size = 692109, upload-time = "2026-10-26T17:11:32.645Z" },
396+]
397+
398+[[package]]
399+name = "idna"
400+version = "3.20"
401+source = { registry = "https://pypi.org/simple" }
402+sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" }
403+wheels = [
404+ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" },
405+]
406+
407+[[package]]
408+name = "iniconfig"
409+version = "2.3.1"
410+source = { registry = "https://pypi.org/simple" }
411+sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
412+wheels = [
413+ { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
414+]
415+
416+[[package]]
417+name = "interface-meta"
418+version = "2.0.1"
419+source = { registry = "https://pypi.org/simple" }
420+sdist = { url = "https://files.pythonhosted.org/packages/28/5b/202a48a1ccdef721a95357fbe263c54ef87c18c4972df1ab5c22fe0f33d5/interface_meta-2.0.1.tar.gz", hash = "sha256:902bd9a95a12f195f15753a1080075d4eca7a2cb934fac7ac03c9e362b50796a", size = 15281, upload-time = "2026-04-30T22:35:36.882Z" }
421+wheels = [
422+ { url = "https://files.pythonhosted.org/packages/a1/d3/20a61a248feb1249cd81f83ce731f0bdf5170ed96a08a298f7081cda9e90/interface_meta-2.0.1-py3-none-any.whl", hash = "sha256:f38016bef9a4429b6d0792d809be7b65e9781820c674bf7f463999086b6e6323", size = 15330, upload-time = "2026-04-30T22:35:35.201Z" },
423+]
424+
425+[[package]]
426+name = "jiter"
427+version = "0.18.0"
428+source = { registry = "https://pypi.org/simple" }
429+sdist = { url = "https://files.pythonhosted.org/packages/63/5b/f68319f4fe624311a744a33d10fdbcac4c971991e348223cba60d31780b2/jiter-0.18.0.tar.gz", hash = "sha256:a0ca63fd9450a212e4e658d4cfe0ab4977c4f9b0003c68828b843c65e27662ff", size = 234470, upload-time = "2026-10-23T13:51:13.103Z" }
430+wheels = [
431+ { url = "https://files.pythonhosted.org/packages/45/0e/075bfa4fa46820da9ac8cf8a67c435b0586012279fba15deccde0e73b9ce/jiter-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:77a01cdf4381a1becbe5b827f682006f47b712581667451c5b1eb259b9ae7041", size = 290095, upload-time = "2026-10-23T13:49:14.118Z" },
432+ { url = "https://files.pythonhosted.org/packages/2e/e2/f4000a4d9c519ca019bf0e2922cc0c6b6db347be6303161382eda7635be7/jiter-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b41cd64ee6b54db94bea6274e6e37002ed526aa06c114a8be6f36ffe21c184e", size = 316319, upload-time = "2026-10-23T13:49:15.263Z" },
433+ { url = "https://files.pythonhosted.org/packages/51/93/9aaef4d18d19446fd044b9cff0323b1b45b5526f077f1a80418567a6eef8/jiter-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7424194e68ef44e94290961f59319850d7ce11a329a3957f91709c15ded6618", size = 335631, upload-time = "2026-10-23T13:49:16.551Z" },
434+ { url = "https://files.pythonhosted.org/packages/fd/9e/d2dc5e8179e248df7ef1c7c8822554f4c7f694a33be30790bb523f1e657a/jiter-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04a484fce032a8b2c21dec0410a805d7b16573eb34297156a3b4d242c507d2b4", size = 322596, upload-time = "2026-10-23T13:49:17.875Z" },
435+ { url = "https://files.pythonhosted.org/packages/22/ca/28bfd20743be0b2b37c1ddfe069a637d5180e535f1c8faad555cf7ac0b98/jiter-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:860ac97858116bda0ac02ef5f50c541fc1a84ad7b9368cac68d4de5697a9012d", size = 333858, upload-time = "2026-10-23T13:49:19.343Z" },
436+ { url = "https://files.pythonhosted.org/packages/69/89/e42f7dba54dcf7787aa4be3e22cdcb1f14d9635b6527949652ab490d6f8e/jiter-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39ef656cf371971486ec545ca0ae2da8d037d5479bc906121e3e46de86ee73c4", size = 348656, upload-time = "2026-10-23T13:49:20.796Z" },
437+ { url = "https://files.pythonhosted.org/packages/74/6a/b13407f609ddaf9a1f0cdf1336326dc5c8c7fc56943e8450a04a542f5edf/jiter-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c741e6b778c951a6861964ee475118812a33208426c54fb92bf4e318bda834", size = 336286, upload-time = "2026-10-23T13:49:22.152Z" },
438+ { url = "https://files.pythonhosted.org/packages/b2/7e/b560f97495a485a569e7fa338a13587328854f6a40cd2e93602c327ca4a6/jiter-0.18.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:1277ac083a58d2f49a24ff9a28fd9876a6443a1992d4467a1db80fb45e354b18", size = 314415, upload-time = "2026-10-23T13:49:23.645Z" },
439+ { url = "https://files.pythonhosted.org/packages/8b/97/457b6ec16f1f3f25ec760f23728f43a2b20ddf934fcd8f02c81a2183597e/jiter-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ccf224d2e706be81d464b59c2bc3f98ba65d12c092c1789e625127e64488944e", size = 332078, upload-time = "2026-10-23T13:49:24.917Z" },
440+ { url = "https://files.pythonhosted.org/packages/0e/d9/6f616071650f7bb8fcbce674d4dde7e59cfbd8a4d30f5c71c03630425d6e/jiter-0.18.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5c7fc8e5357d1ad082bb177be71e398fcd3d458726439255ca5cb0d706060269", size = 480354, upload-time = "2026-10-23T13:49:26.221Z" },
441+ { url = "https://files.pythonhosted.org/packages/42/72/52a3630c8213cdc8093cc86dc6ad50f2bc89ccfa97ef272205d38a24e16d/jiter-0.18.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a6bba0404b46157a651d1fe512cf2c59c8e42836a9284c35cdff35310758ba4f", size = 515322, upload-time = "2026-10-23T13:49:27.652Z" },
442+ { url = "https://files.pythonhosted.org/packages/31/54/987cfee52bdf8a286e9c1c58192a9bb64f8773a6be36e4ec46959f67fb7e/jiter-0.18.0-cp312-cp312-win32.whl", hash = "sha256:07ae058bc052ead609b49bbae78c01bf4dee8f5f9d21705cbae9bfe59b33efc9", size = 185341, upload-time = "2026-10-23T13:49:29.147Z" },
443+ { url = "https://files.pythonhosted.org/packages/c5/c9/b1279eecf68e5f62c918af3f7f856a071ae7db0c90b8fd2235b1b6020c8c/jiter-0.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:79d1fe529bd7065c6f3f50279a08c2ab82eaa0631a6f5dfafa9c65105eeaaafa", size = 225889, upload-time = "2026-10-23T13:49:30.306Z" },
444+ { url = "https://files.pythonhosted.org/packages/e0/14/715f84be58a7fc97779aa40c37164acfa25ccb2cee89bfb69358e7071ff3/jiter-0.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f8b3b3d8761dee3cddcb5d3e99eaccc0155bbefc345affcdedcc758a4933a1f", size = 181852, upload-time = "2026-10-23T13:49:31.811Z" },
445+ { url = "https://files.pythonhosted.org/packages/50/94/aa275cdb099fd84e402977f8fc8628e2cee2f7c8eafe1d1d5c5e722f5ee5/jiter-0.18.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1517648b5f6268513dea206241a99d9991d6ffcbdbc406848e77a7f02ed3ccf1", size = 289648, upload-time = "2026-10-23T13:49:33.059Z" },
446+ { url = "https://files.pythonhosted.org/packages/8d/30/3eaf67279c6183393d3608b6713c93668ae82ea59f0589ea59d8176d8741/jiter-0.18.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7161217e2190b9b2c8c79b81699be29d67010f6f77e09a53a3b62261792d5489", size = 315919, upload-time = "2026-10-23T13:49:34.457Z" },
447+ { url = "https://files.pythonhosted.org/packages/f8/2b/d18adcfec3516b97b24c0d5cd1b0ced22cada8ea9806f21bece75af49ffe/jiter-0.18.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9623fc90f1a4754452a525383ce70476476c16556098944709b8cdbd9ad905f0", size = 335231, upload-time = "2026-10-23T13:49:35.776Z" },
448+ { url = "https://files.pythonhosted.org/packages/41/e8/42cff1077883ea5311621dc9384a1ede4799bc3c01411798ac75f7a71f15/jiter-0.18.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9083a7c2a4ee6358bc0401c46b4d5090f208f6fadfb6275c7b5fe3217e13506", size = 321590, upload-time = "2026-10-23T13:49:36.997Z" },
449+ { url = "https://files.pythonhosted.org/packages/1d/4d/2fbedfce563223d4783a872cead5f5bb74d2d1e418f963410ca28e4b7394/jiter-0.18.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4632f5172800da4cd42fd53f4553c772acd08f1c33dd7ac135e29d735080fa0d", size = 332960, upload-time = "2026-10-23T13:49:38.615Z" },
450+ { url = "https://files.pythonhosted.org/packages/04/a0/b082b2d8bd3bef9a888cc0a0f060a24644f24b5195f2e8227a40507e6b71/jiter-0.18.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c00b1f8fc1dd06c610305be36778b38148ed1e9a532bdd81189c3c0d9905c888", size = 348149, upload-time = "2026-10-23T13:49:39.826Z" },
451+ { url = "https://files.pythonhosted.org/packages/64/11/babc8d3c004bb2f11b7e3adfe27a5bc7c1e13cdfc6360d424c57355c55ff/jiter-0.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e3350b7431f7b8315b925d9ca0f29a44e8fb6ed45c1fe552392cb6297966bb9", size = 335978, upload-time = "2026-10-23T13:49:41.165Z" },
452+ { url = "https://files.pythonhosted.org/packages/3b/21/c50bf83c39d60f0df896ebcb025b316b670a703397d08bea2c69b9615101/jiter-0.18.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3b49071fb7917cdbc6d80c81a491c6ad146c1a8fd6cb3076cd3c1bfeb6e266f6", size = 313767, upload-time = "2026-10-23T13:49:42.538Z" },
453+ { url = "https://files.pythonhosted.org/packages/af/19/3f66480d5bfd3f49571a073eefac4d890dd590a8eb3c78b15e0293909cc6/jiter-0.18.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04e210f5ddcccdbde484821040216a112381e6484abc549c20c38e9698ab07ca", size = 331070, upload-time = "2026-10-23T13:49:43.782Z" },
454+ { url = "https://files.pythonhosted.org/packages/5a/3d/b5662c84b14475c8b018ed1ca2ea44069194b7d97a03bac81ae6b46f6b9f/jiter-0.18.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5b339f6ae14fe7be92f391cc65cc5c80ff76e8da76d63b18fc14664f09b7cece", size = 479939, upload-time = "2026-10-23T13:49:45.033Z" },
455+ { url = "https://files.pythonhosted.org/packages/80/b8/081c0414379ee2564aa7808f78e963538a2c4aed2f8d31775e2423e1cdbf/jiter-0.18.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:5cb43336244e063f9cc11e44730e4b92dea70c6eb9b649d268c5ebebc4bc7685", size = 514734, upload-time = "2026-10-23T13:49:46.236Z" },
456+ { url = "https://files.pythonhosted.org/packages/0f/1c/6f7bf98786851ae424a1d88562586302536cc137848fd12d2add3960fab3/jiter-0.18.0-cp313-cp313-win32.whl", hash = "sha256:9656d0e9001f69c4af6523874ff445ab4e5db7b835f59733cc5ff51e73a9aa57", size = 184699, upload-time = "2026-10-23T13:49:47.398Z" },
457+ { url = "https://files.pythonhosted.org/packages/38/53/5803216c024a241a0bf41ed2668713d7228f92c5593e40d5e873e3b20e0b/jiter-0.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f26198220165eb9627847401fff4d0ca8bebf3ef840a88c6312918d2291c33a", size = 222493, upload-time = "2026-10-23T13:49:48.533Z" },
458+ { url = "https://files.pythonhosted.org/packages/fe/8e/484919ccb041c9a1e84059be02bd37ff086b8b084e21630e0958d3ae41a3/jiter-0.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:bef1fe687dc6af17bef9f644a95d465f15f0efa20b743c858e0b6a14e2c0c73e", size = 181250, upload-time = "2026-10-23T13:49:49.777Z" },
459+ { url = "https://files.pythonhosted.org/packages/74/0e/45a502771d73d4a939132d8bf85d7b049c7ca06a07f84b414b525c3dc084/jiter-0.18.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:248e939f67a9f8030e2624260a9f417decfe275af37aa96728d256468a9c4086", size = 291757, upload-time = "2026-10-23T13:49:50.95Z" },
460+ { url = "https://files.pythonhosted.org/packages/21/f6/d9cfa2bf2a969fb8e002fb8d8a6cd98d49a6248cb7e4ffe2247766af13fb/jiter-0.18.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0390fa550fcafbf69fb97ccb287bd71192f90410a1c7dd600ca4000aa9aabcf2", size = 318585, upload-time = "2026-10-23T13:49:52.407Z" },
461+ { url = "https://files.pythonhosted.org/packages/46/38/ea1dae56f123e3a864188cbbc577314293c0dcc019a880fa9c0a8097934a/jiter-0.18.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8013186f2b9a8c27ededa601050392527aa9d27ffd35d49aae424fbd2062f38", size = 338099, upload-time = "2026-10-23T13:49:53.658Z" },
462+ { url = "https://files.pythonhosted.org/packages/1f/d9/fcc5c53a32f98d52f4fd46fda62b69f5c7c2c975a582c0e18501250a4f78/jiter-0.18.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd4316df8e2074fcf5173f6001f1fc9596e7c1e8c4bf499b02f9ebe92babdfb1", size = 324016, upload-time = "2026-10-23T13:49:54.999Z" },
463+ { url = "https://files.pythonhosted.org/packages/d4/63/c24cd08907f08e6a3ba3173616ad268a3d32dd81c63ba34b991d08577d28/jiter-0.18.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9e474da7a9f65d3b460cca93cc8a0f4ec0220d1fa41f1f7a3709195ead60056", size = 335904, upload-time = "2026-10-23T13:49:56.214Z" },
464+ { url = "https://files.pythonhosted.org/packages/0b/78/19ca1c8fe87bd3c8540b91504ff2dd52c9c538233044c4cf787fc1fab39b/jiter-0.18.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d55acd326db8ecfc94f21951abd20902c7939d35925444e48d94527e287bead7", size = 350523, upload-time = "2026-10-23T13:49:57.552Z" },
465+ { url = "https://files.pythonhosted.org/packages/c3/27/84be9cc1c2b781d2da7c694fd7608311af3aaab91f20eb00d4999285b351/jiter-0.18.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01642d403b8925f2b1e1bb51b4ba49ece8a2bdc9b9560fc8ed0b1f2b35b567a", size = 338324, upload-time = "2026-10-23T13:49:58.842Z" },
466+ { url = "https://files.pythonhosted.org/packages/9a/ac/7b3bfb49b19e42ebc2ae56ec307c372602dd1e17892f7e051c9bf97cd7e6/jiter-0.18.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:89ba6b674d4bd7290f9943bbeba0e1bf35955bbb1e07e41ba94691648cf133d5", size = 316484, upload-time = "2026-10-23T13:50:00.257Z" },
467+ { url = "https://files.pythonhosted.org/packages/f9/6a/e37f50885aa4daa1dfe413784692ead6b48a1d9e857cd394a61bf345189c/jiter-0.18.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:feadebdfee32c61e91066433f4aa74c891752e26e43032c1feef08dda6d9b6ed", size = 334225, upload-time = "2026-10-23T13:50:01.542Z" },
468+ { url = "https://files.pythonhosted.org/packages/8b/f8/1026bc92f0fd443a1137993d1448a7d7c4b7a34bec2722a1533302ebc4b4/jiter-0.18.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:dd7e57efd54a3580620f635c48a3226f910e23649b7eb83ffbc6f0f3386293b8", size = 482157, upload-time = "2026-10-23T13:50:02.849Z" },
469+ { url = "https://files.pythonhosted.org/packages/1b/2d/61f3c35736873c34eeee3d0008067febd88ded8e8cf6f9a0ab090348a787/jiter-0.18.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ea69e23481497c2143775a98d273ccc43de5c92a6387df18c903d2d37bc2b181", size = 516956, upload-time = "2026-10-23T13:50:04.19Z" },
470+ { url = "https://files.pythonhosted.org/packages/6b/bf/9d14be58e76324f4927c7f8e5dbe2f9fd04faaec884a6465173f8142eac1/jiter-0.18.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:d9b61af76a3cd90ad9656a331737d97dab8c05facb2d04cb41fb16c9adadc966", size = 112087, upload-time = "2026-10-23T13:50:05.423Z" },
471+ { url = "https://files.pythonhosted.org/packages/c6/7d/cff751c560d514da283bae595fe154714f285ec5ac15e2b16e9e0c0c8948/jiter-0.18.0-cp314-cp314-win32.whl", hash = "sha256:fe42726d7fe171cd8fc1966d4457a6aae03bd408d519d2cc5e7726805460b51e", size = 186562, upload-time = "2026-10-23T13:50:06.81Z" },
472+ { url = "https://files.pythonhosted.org/packages/04/70/4e7f51e517be4146400fd1ce642d535380a38c7db5ab475d38bbcd16a82d/jiter-0.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:f1e738b851019d6bb6ff4a42c6319bdede967dec958ef3ec6feb7ab9d12408f7", size = 225448, upload-time = "2026-10-23T13:50:08.302Z" },
473+ { url = "https://files.pythonhosted.org/packages/78/43/02bfb85f63e04c87c406731197e41f2e6b415035e41497428e0422fb326e/jiter-0.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:80a961b1c5c7786bc03b332deb7f7ebe818b7baa6e26685e2bf480548ca128b4", size = 182907, upload-time = "2026-10-23T13:50:09.795Z" },
474+ { url = "https://files.pythonhosted.org/packages/9c/66/5cbde35cbb14c8339af39c9bc61cbda08b212116f96603d23b7c9cc84a4d/jiter-0.18.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b066f37dd9ec19fc7931d79dee38f56b6f849217bbe824a7dad4518ef96ee515", size = 318962, upload-time = "2026-10-23T13:50:10.91Z" },
475+ { url = "https://files.pythonhosted.org/packages/eb/d1/dccc47ed6070d3b25622823048a80a49fe87ccd737e5d66b802ca20e4fde/jiter-0.18.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccdc09887b04f50f1761eedcfd19da882d9d834096bc1215049af637023f7db7", size = 336718, upload-time = "2026-10-23T13:50:12.149Z" },
476+ { url = "https://files.pythonhosted.org/packages/de/a5/8d4ec06d7a0f2bccbde8508ed847ccd323a9dd1fbab5900cda3fc5bd71f8/jiter-0.18.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6133abcd0f332964fbb8145adb1ce775cd0312c9eaf6e4bf26b7a5c9b6f7cca7", size = 322945, upload-time = "2026-10-23T13:50:13.591Z" },
477+ { url = "https://files.pythonhosted.org/packages/7f/50/49d06214b30c685d64ffdd40fdb50a98b6ccff4b012fc9ed491d44de9ef1/jiter-0.18.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca381b4a466023d1fdd04c889c759b46f7d9654f622b28bb01e65abf31d07bba", size = 335325, upload-time = "2026-10-23T13:50:14.807Z" },
478+ { url = "https://files.pythonhosted.org/packages/f4/9a/4ecb866a7a01a4e76d9ffbde3fac2cd66ad978c8c797953d44f91fbf2040/jiter-0.18.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b961fedf589ffa182fe302fc4371acf4606d3ed4a6a48a151fbfd724d811f5a", size = 350578, upload-time = "2026-10-23T13:50:16.125Z" },
479+ { url = "https://files.pythonhosted.org/packages/16/7e/c8b5ecb6af4c9454376263f167f3f8bb5dec41b10fac4f48ea4557b9f115/jiter-0.18.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b77eabfbae1ac47872be0076899e66340bbef39b862971c3d6c9a4b7c91fd10", size = 338791, upload-time = "2026-10-23T13:50:17.703Z" },
480+ { url = "https://files.pythonhosted.org/packages/8a/04/45ba31ce79022706323fd2291f87d956fd51fdfcba2ba8924b3de62f0ce2/jiter-0.18.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92a16933d0fa852133500837d33cd6a4f75e4414d77389c58269e12bafc2ee02", size = 316169, upload-time = "2026-10-23T13:50:18.915Z" },
481+ { url = "https://files.pythonhosted.org/packages/62/c1/fd36126144f4c32d1b76939278e9d3b7b798075c0478b641d6066621bafd/jiter-0.18.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f7ba518893a4ccef5afb38e41bc3e126bf652354417ec4e75e5a5b000fb1ef6", size = 334754, upload-time = "2026-10-23T13:50:20.169Z" },
482+ { url = "https://files.pythonhosted.org/packages/cc/e2/30a5ac1a3ee9e01bda75a4fcb55bbeddcf8875f8ce7e680b787c177f14b5/jiter-0.18.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:04d67a44b040a6662a35edc9869030f7aa566bf5baaf3549c99d847aa44213e2", size = 481094, upload-time = "2026-10-23T13:50:21.481Z" },
483+ { url = "https://files.pythonhosted.org/packages/33/de/5e736a36eff6933da012dc440540f79e3ccfc06e27f557638280657df023/jiter-0.18.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e4d4819398d5330636bc0b91ccc0048dba782076fb99257038257ba6f121c840", size = 515524, upload-time = "2026-10-23T13:50:22.872Z" },
484+ { url = "https://files.pythonhosted.org/packages/0a/ea/1e8bc5b0c920a13e5b698106dbb2d2da5c5e3474668746246f649abb6d5f/jiter-0.18.0-cp314-cp314t-win32.whl", hash = "sha256:c435e795a7ea4b372166a058a5b49a25caeb9c94e469a1414868b14c83ee5c41", size = 185058, upload-time = "2026-10-23T13:50:24.098Z" },
485+ { url = "https://files.pythonhosted.org/packages/1e/0f/a742b397699c3af52dd95443332164884fe6204e5faa5321b7c073beab0f/jiter-0.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5a3d5397da7ae46c7a9601ef02d2c9ec47a6c3abe15e1ea940eaf93e675ee20e", size = 225598, upload-time = "2026-10-23T13:50:25.44Z" },
486+ { url = "https://files.pythonhosted.org/packages/b3/b7/08ae26347dca48d5cadca33be2f2f5b6b1ae790b322109083d6f95959feb/jiter-0.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4b81b474e4a8ec745ab20a6762275a360e1147cb70cf08872ec14e1aa5debc37", size = 183052, upload-time = "2026-10-23T13:50:26.675Z" },
487+ { url = "https://files.pythonhosted.org/packages/c2/d6/0d2a8a9747ad918141827fabdc05abc3bf73986f7f36647fb8c19708e21e/jiter-0.18.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:30f8ae18425e0247ffff4e4a5753ec1952e1b81ed4e7949b84e5ecfaf8315833", size = 291908, upload-time = "2026-10-23T13:50:27.94Z" },
488+ { url = "https://files.pythonhosted.org/packages/54/b4/19c582727e22681c6069db037c3b3e354c6fbdce7fbc5290a7bc7bc5884f/jiter-0.18.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:93150a3bb0317d8f5a94d4c3ea68db1cb434b94ac819e08c443cea97b7645f2f", size = 316544, upload-time = "2026-10-23T13:50:29.167Z" },
489+ { url = "https://files.pythonhosted.org/packages/c3/62/5d34e93dc5d1e707788a378335efc876cdc731ceb2111b0a9d7c6f9fdada/jiter-0.18.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c8a61d6227cdcb0a67c678f30a020837c9f3dd7e0b3f916781defa157e4eb9", size = 338101, upload-time = "2026-10-23T13:50:30.457Z" },
490+ { url = "https://files.pythonhosted.org/packages/75/e2/0b16a6ca5479cd9efa3c59497cd42f1979a539b6c664a5b9c87e3632f176/jiter-0.18.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:285cc15ff9ffcd4ce7f0b9ce02fcbc65e22fcf7dfbda1a3d71ccac8d7b73f8a1", size = 324213, upload-time = "2026-10-23T13:50:31.695Z" },
491+ { url = "https://files.pythonhosted.org/packages/85/9d/620a97457a0210092bf9faa1eb30f5da094fab2de07a9478af7c254e22fd/jiter-0.18.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8f69ef2e28f8c1759bc1522385ab3e4ade0b1056053cecf67449a947c4525a8", size = 335777, upload-time = "2026-10-23T13:50:32.984Z" },
492+ { url = "https://files.pythonhosted.org/packages/76/08/a3514df093824f10a61dc1d0bbc22285ed779ba42f15d727499a8c877e16/jiter-0.18.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eec2da60423287bd60377b0fad1723560dcc51cfb2b99615be8431ad425c4438", size = 350490, upload-time = "2026-10-23T13:50:34.421Z" },
493+ { url = "https://files.pythonhosted.org/packages/cd/60/8f8b51bae62259b429e83d0adff69d93e39e4fadbc4bd1856dc2a0950639/jiter-0.18.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5db2cdae6a2f01e8f52ab2154886dcf5bb1844b4f31245440bf623ec91b65b08", size = 338591, upload-time = "2026-10-23T13:50:35.759Z" },
494+ { url = "https://files.pythonhosted.org/packages/2e/ed/5307f0eb051d987510fdd1020c904927c0b364a4937d372817bdefaced31/jiter-0.18.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:029da1f29852beac2f9001f20209b73b29b4d7bdc748c7f8028895769e2cc0d8", size = 316623, upload-time = "2026-10-23T13:50:37.316Z" },
495+ { url = "https://files.pythonhosted.org/packages/f9/0e/4bdc945ffbf77c60f07c726fbeb45508018322df9d9942d262256397062e/jiter-0.18.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:67c6662b1b7e5728a507db06ad1b8a7ff8ade04c2a74f092d467209ef744d3a4", size = 334720, upload-time = "2026-10-23T13:50:38.507Z" },
496+ { url = "https://files.pythonhosted.org/packages/4b/33/fb3e8f9354e7edab05f3752383eac58ce6e785f2f1626b697dd5dd0a86e4/jiter-0.18.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:4db12daef4a231df528a16f70d5ff7f5e3be21f70e8ad904e4c61442366e8a10", size = 482130, upload-time = "2026-10-23T13:50:40.023Z" },
497+ { url = "https://files.pythonhosted.org/packages/2e/3d/3aef2be95155bbacadd8691208b6f0f7371cc0cefe80241a1245e38e308d/jiter-0.18.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:4654ea93f525ea96adf289afcf1ead32d773b8325ec326862b4f81f5f4570835", size = 516986, upload-time = "2026-10-23T13:50:41.279Z" },
498+ { url = "https://files.pythonhosted.org/packages/3c/f6/2aa54fd5da3d0a93de2655e2525426263168454259ba3f2fb70d1b36fbc9/jiter-0.18.0-cp315-cp315-win32.whl", hash = "sha256:f035d01b450ac4e141502f1ce142462cee638198ff5a0237c80e6683798b3b87", size = 186645, upload-time = "2026-10-23T13:50:42.526Z" },
499+ { url = "https://files.pythonhosted.org/packages/b5/77/5b54ca8c292b83991cafbac6b022f515e46f1ae77dd5f4a97cc33644b235/jiter-0.18.0-cp315-cp315-win_amd64.whl", hash = "sha256:4a18ef4fe95685b000d95eca797cc683e0c178a4b364f2e39f6c79ab107a26e6", size = 225087, upload-time = "2026-10-23T13:50:43.779Z" },
500+ { url = "https://files.pythonhosted.org/packages/9f/f3/53b7f39b83c757380ab57d9bc3f934431d6b6751f007e8122f7f63c10c2c/jiter-0.18.0-cp315-cp315-win_arm64.whl", hash = "sha256:c73922e4ff4b1ad2929cd795c98223c6688a43c5fd42e9deb8955e4d43b00abb", size = 183120, upload-time = "2026-10-23T13:50:44.985Z" },
501+ { url = "https://files.pythonhosted.org/packages/77/f8/3a3ef55c528722f3b4be0e7b9686a37ddbe86ed96510bc1c2a63dab7f14b/jiter-0.18.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0ccf55f6fcdcee82fe40ae617e9d64b8e5f433fa6cb5bf7d2969e584e6df0e5d", size = 317996, upload-time = "2026-10-23T13:50:46.266Z" },
502+ { url = "https://files.pythonhosted.org/packages/e1/15/b06c1c17bed30b5c5dabb40f80a56a3b71b89c16ae86552307f7ccedc93a/jiter-0.18.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:163a55e77f8e92d739b1f4ef212f1d445ddc58ecdf0da1feca44ef424b5331f8", size = 336638, upload-time = "2026-10-23T13:50:47.444Z" },
503+ { url = "https://files.pythonhosted.org/packages/87/79/b7bcd3e8c850326a323d18a5607334ea40659e22a4fc321e315d1d230007/jiter-0.18.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9de6c9133be12a51ea2d8c270f39a04ed45ee369802bb6270649b952e6cec256", size = 323116, upload-time = "2026-10-23T13:50:48.715Z" },
504+ { url = "https://files.pythonhosted.org/packages/5e/99/4c53c9d46462cd0691c3c188f89612e63a424d19f5befd1b0631f8f90638/jiter-0.18.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ea4f2ed18eba7454182d4ff727444788ae51ad11bf5a25ad892f5f0ad3e754b", size = 335162, upload-time = "2026-10-23T13:50:49.987Z" },
505+ { url = "https://files.pythonhosted.org/packages/6f/ab/ef873065a3a1c442a4e62558b77bfa338066317dbafdfdc991121201f5ad/jiter-0.18.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:626d5c9fb8b57babc6ce9d03224846ecb094e1d22ff5bff6080398e7f8aee162", size = 350560, upload-time = "2026-10-23T13:50:51.478Z" },
506+ { url = "https://files.pythonhosted.org/packages/19/7e/48e78558d7cb3d289211da0397b7ed31cfe3efc98dd85f2324abd574c07c/jiter-0.18.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c3318cc6ef857b6554b43479c30e8249fc143189d500d4137cde9c58412e6d", size = 337848, upload-time = "2026-10-23T13:50:52.961Z" },
507+ { url = "https://files.pythonhosted.org/packages/2c/35/b0a093e72def7caec970dd8536c3d42af3add3aedfb3c06ac0588effde26/jiter-0.18.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:9a70f25f6de76a01e309e109c9bcf2143c5c126b3488dd17a25c7aa404b28f16", size = 315870, upload-time = "2026-10-23T13:50:54.152Z" },
508+ { url = "https://files.pythonhosted.org/packages/48/a4/fd7ca4d1ce8b876be7a260f5e31615854aed5b041553a63e85c4a265cb63/jiter-0.18.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22d8dd757e603224e46786d63cb7d7bfbddc8f579a02e7b082840468d18c06f3", size = 335253, upload-time = "2026-10-23T13:50:55.427Z" },
509+ { url = "https://files.pythonhosted.org/packages/f5/71/ccd7154afecd4687b22a8195d5dc6460f89f6e17537ec78edb58a2faac34/jiter-0.18.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:552a8714f26ddcb43115c82da6d045b3b9f2fa5997d8b21b2f8ec3721503bebb", size = 480775, upload-time = "2026-10-23T13:50:56.91Z" },
510+ { url = "https://files.pythonhosted.org/packages/c8/b9/bad5391d7fb734cd6ebc383c8603bc324518bf4ce865f5d813908ae42596/jiter-0.18.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:8060a206e290410d3bfc3bd91715f3ec90a499246f8335d48927e0b3b6a6ccc5", size = 515619, upload-time = "2026-10-23T13:50:58.231Z" },
511+ { url = "https://files.pythonhosted.org/packages/c8/5c/9596452ad56340e6ed395c5fe27ab102bb2aa0c4b59de2e6a8b3dc3b03a7/jiter-0.18.0-cp315-cp315t-win32.whl", hash = "sha256:91d463858f11ee76ce8e39f391bbcbf1d4c3cc4703a1496fed3a5666dcd5e01d", size = 185204, upload-time = "2026-10-23T13:50:59.456Z" },
512+ { url = "https://files.pythonhosted.org/packages/08/c8/62c798e2a1a6f827eac1d45232c93b1b0e387a22d559c8483dc7302d5fd5/jiter-0.18.0-cp315-cp315t-win_amd64.whl", hash = "sha256:6ee49daab0b826279c152ad8d61171ea17f4197f91a5e14251d0d5d4adf51059", size = 224948, upload-time = "2026-10-23T13:51:00.624Z" },
513+ { url = "https://files.pythonhosted.org/packages/64/42/cdfa494914b6d512431e795d8d6226b84b77b733bb8b5eccafd5a1a86dad/jiter-0.18.0-cp315-cp315t-win_arm64.whl", hash = "sha256:bec676a0dc24d4dabd4bdfb3d1f1bf80d4d3dd892fc5c5b3f8f953e2459dbfce", size = 183182, upload-time = "2026-10-23T13:51:01.797Z" },
514+ { url = "https://files.pythonhosted.org/packages/a0/c2/49ee8f4743c27e61c52b4746ba636b43bedda97131a14043dc6f5ce5bb9d/jiter-0.18.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:118c1fd995f349f1cad0557f31d6860f336b4aeef7f002dedbc068027f68e2c3", size = 287302, upload-time = "2026-10-23T13:51:08.073Z" },
515+ { url = "https://files.pythonhosted.org/packages/60/72/394036294ac3226216f73bfa4b14df920eb5baf7fb80fa7e0a0d7ef442a6/jiter-0.18.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:44d0d719cd7845e66b01a0cf346ec9882073e710a05e89a5b6d87be47a9dc69c", size = 277797, upload-time = "2026-10-23T13:51:09.324Z" },
516+ { url = "https://files.pythonhosted.org/packages/ce/80/f0dadedaa9a109f31e8978c7db355593916968d5956f382188a3f8c0888c/jiter-0.18.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d1d2dc4d930ebc225f488b6b3790acebb2e51b3a165a4a42cc1d05bb927f83e", size = 300192, upload-time = "2026-10-23T13:51:10.551Z" },
517+ { url = "https://files.pythonhosted.org/packages/93/51/e57dbc6c264ec6557a677189755fe4525d83439237418e3ae59771ac1cde/jiter-0.18.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb0c346b6c5d9c033fe9e9b6ed6a1ff6dac0288fe681c3434dde6eb9c264fa1", size = 301141, upload-time = "2026-10-23T13:51:11.882Z" },
518+]
519+
520+[[package]]
521+name = "kiwisolver"
522+version = "1.5.1"
523+source = { registry = "https://pypi.org/simple" }
524+sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" }
525+wheels = [
526+ { url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" },
527+ { url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" },
528+ { url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" },
529+ { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" },
530+ { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" },
531+ { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" },
532+ { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" },
533+ { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" },
534+ { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" },
535+ { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" },
536+ { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" },
537+ { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" },
538+ { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" },
539+ { url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" },
540+ { url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" },
541+ { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" },
542+ { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" },
543+ { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" },
544+ { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" },
545+ { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" },
546+ { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" },
547+ { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" },
548+ { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" },
549+ { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" },
550+ { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" },
551+ { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" },
552+ { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" },
553+ { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" },
554+ { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" },
555+ { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" },
556+ { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" },
557+ { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" },
558+ { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" },
559+ { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" },
560+ { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" },
561+ { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" },
562+ { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" },
563+ { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" },
564+ { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" },
565+ { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" },
566+ { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" },
567+ { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" },
568+ { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" },
569+ { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" },
570+ { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" },
571+ { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" },
572+ { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" },
573+ { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" },
574+ { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" },
575+ { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" },
576+ { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" },
577+ { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" },
578+ { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" },
579+ { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" },
580+ { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" },
581+ { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" },
582+ { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" },
583+ { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" },
584+ { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" },
585+ { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" },
586+ { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" },
587+ { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" },
588+ { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" },
589+ { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" },
590+ { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" },
591+ { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" },
592+ { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" },
593+ { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" },
594+ { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" },
595+ { url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" },
596+ { url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" },
597+ { url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" },
598+ { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" },
599+ { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" },
600+ { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" },
601+ { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" },
602+ { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" },
603+ { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" },
604+ { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" },
605+ { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" },
606+ { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" },
607+ { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" },
608+ { url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" },
609+ { url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" },
610+ { url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" },
611+ { url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" },
612+ { url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" },
613+ { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" },
614+ { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" },
615+ { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" },
616+ { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" },
617+ { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" },
618+ { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" },
619+ { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" },
620+ { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" },
621+ { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" },
622+ { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" },
623+ { url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" },
624+ { url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" },
625+ { url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" },
626+ { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" },
627+ { url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" },
628+]
629+
630+[[package]]
631+name = "market-agents"
632+version = "0.1.0"
633+source = { editable = "." }
634+dependencies = [
635+ { name = "anthropic" },
636+ { name = "duckdb" },
637+ { name = "matplotlib" },
638+ { name = "numpy" },
639+ { name = "openai" },
640+ { name = "pandas" },
641+ { name = "pyarrow" },
642+ { name = "pydantic" },
643+ { name = "python-dotenv" },
644+ { name = "pyyaml" },
645+ { name = "scipy" },
646+ { name = "sortedcontainers" },
647+ { name = "statsmodels" },
648+ { name = "tqdm" },
649+]
650+
651+[package.optional-dependencies]
652+dev = [
653+ { name = "hypothesis" },
654+ { name = "pytest" },
655+ { name = "pytest-xdist" },
656+ { name = "ruff" },
657+]
658+
659+[package.metadata]
660+requires-dist = [
661+ { name = "anthropic", specifier = ">=0.40" },
662+ { name = "duckdb", specifier = ">=1.1" },
663+ { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100" },
664+ { name = "matplotlib", specifier = ">=3.9" },
665+ { name = "numpy", specifier = ">=2.0" },
666+ { name = "openai", specifier = ">=1.50" },
667+ { name = "pandas", specifier = ">=2.2" },
668+ { name = "pyarrow", specifier = ">=17" },
669+ { name = "pydantic", specifier = ">=2.8" },
670+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" },
671+ { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6" },
672+ { name = "python-dotenv", specifier = ">=1.0" },
673+ { name = "pyyaml", specifier = ">=6.0" },
674+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
675+ { name = "scipy", specifier = ">=1.14" },
676+ { name = "sortedcontainers", specifier = ">=2.4" },
677+ { name = "statsmodels", specifier = ">=0.14" },
678+ { name = "tqdm", specifier = ">=4.66" },
679+]
680+provides-extras = ["dev"]
681+
682+[[package]]
683+name = "matplotlib"
684+version = "3.11.2"
685+source = { registry = "https://pypi.org/simple" }
686+dependencies = [
687+ { name = "contourpy" },
688+ { name = "cycler" },
689+ { name = "fonttools" },
690+ { name = "kiwisolver" },
691+ { name = "numpy" },
692+ { name = "packaging" },
693+ { name = "pillow" },
694+ { name = "pyparsing" },
695+ { name = "python-dateutil" },
696+]
697+sdist = { url = "https://files.pythonhosted.org/packages/e7/c8/9aa712a0afb882649424dd8de8ad9aa6235e796e84c6052e8f6dc1598d0d/matplotlib-3.11.2.tar.gz", hash = "sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4", size = 32660610, upload-time = "2026-09-11T19:05:31.214Z" }
698+wheels = [
699+ { url = "https://files.pythonhosted.org/packages/cb/ce/1bfcc4873b121597791ad74032943b123218c0613af0b97e8dd05e916fb1/matplotlib-3.11.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef752769cd962f39ea0b6ffc82d1ea43a0012c5a6157c7a075212fa509cfcff2", size = 9476466, upload-time = "2026-09-11T19:03:19.45Z" },
700+ { url = "https://files.pythonhosted.org/packages/a6/c4/7f5f3601ee69baf072c0c7d3ce60c03e0618621c5a56c34a62a460e29d11/matplotlib-3.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef31985c4dedb5f1424e1aec6849a47dd37689cb7fa3c20b1b82187f26806261", size = 9305583, upload-time = "2026-09-11T19:03:22.39Z" },
701+ { url = "https://files.pythonhosted.org/packages/b8/90/2b3fd67ee273163faeda6d514be70b5596eaa0fd77b60ffc294ad0b34f5f/matplotlib-3.11.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df4f7784aca81a94f254c0a2767d592ee25f407e488f5fa7203e51093fb6ca27", size = 9860353, upload-time = "2026-09-11T19:03:25.049Z" },
702+ { url = "https://files.pythonhosted.org/packages/f4/84/32549e7a462dc311aed2ab62e5d2538028840b5c53a8be0195c789937c3d/matplotlib-3.11.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b9a7ad579856284135e401ecc918c5f8a017ee30539298862a109f51b971710", size = 10670348, upload-time = "2026-09-11T19:03:28.108Z" },
703+ { url = "https://files.pythonhosted.org/packages/84/39/02e21b74f7439bd643d717ea846006d46e309e6d29b632b57751265311d6/matplotlib-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3aa4b8516fd26659e4363abbf317c703d9116496c5db2e9d0609a2866dd39dd2", size = 10810580, upload-time = "2026-09-11T19:03:31.402Z" },
704+ { url = "https://files.pythonhosted.org/packages/04/93/0d239bde12b308262265c2d98909b2f0ecad2b5deeae96241642e822e9c7/matplotlib-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:c5c1c68ee401fc98271263410f0e5ce88285abacf7627132914e8adf3d70ff43", size = 9349409, upload-time = "2026-09-11T19:03:34.195Z" },
705+ { url = "https://files.pythonhosted.org/packages/49/a8/06baf901c02246c8a222b655cc4540ef9c15e1549a04e07927f9cde716c3/matplotlib-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:643ff850d8e0f5b8319337f87ed3cb59506afb3df3cc48de777d85871233be7b", size = 9028084, upload-time = "2026-09-11T19:03:37.032Z" },
706+ { url = "https://files.pythonhosted.org/packages/bf/ce/fc58855a3d17f6cb8a87b6f9ccd100f65432a3a7ffed57309cbf14c78987/matplotlib-3.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d43ff8cebb50840648cb6429b2228621dbd709010f3117b3740abb20abf21c0", size = 9476987, upload-time = "2026-09-11T19:03:40.17Z" },
707+ { url = "https://files.pythonhosted.org/packages/a9/29/2a47b700545aca050e0ed73c89ba21e08407e11f5f42a567421b220de6c1/matplotlib-3.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a8285cea8ef4d92aa041d1c33788bcae82248503300f93f1ca2136b9049452f", size = 9306050, upload-time = "2026-09-11T19:03:43.103Z" },
708+ { url = "https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1944895967f87c84c9b4bad29a707b31f5b36df4a3a2339ea1f8ea3ce5105539", size = 9861060, upload-time = "2026-09-11T19:03:46.044Z" },
709+ { url = "https://files.pythonhosted.org/packages/e4/b9/b978de6d43d47f1d4d8e653fac32c16d8d2e5f46bbbb822e26d7cb1adece/matplotlib-3.11.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af2661f6ac6bbd1d081996f54fdd9385715c625ace8cec0869055c0cfbf38981", size = 10670885, upload-time = "2026-09-11T19:03:48.942Z" },
710+ { url = "https://files.pythonhosted.org/packages/a3/80/d72f61bb2631ac15d17fada1f17e0f99fecba69d8376d34f17cf93b7eddd/matplotlib-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e5a90f8a707ebb6004a713b1cff091a40cc5df4c7c1cb165e5a505ebc11c292", size = 10811222, upload-time = "2026-09-11T19:03:51.665Z" },
711+ { url = "https://files.pythonhosted.org/packages/94/2e/6d1d33f242c705b5c6c6fb3f0fc141add51cb1abb62912e59de0835d28c4/matplotlib-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:bc067c462a86f0e57bf52fc6e058d90171a5420007dac00c46e90153050c69a7", size = 9349539, upload-time = "2026-09-11T19:03:54.534Z" },
712+ { url = "https://files.pythonhosted.org/packages/bb/bb/c0aa86a7f4c0e62f1c3c0711e361f6f23f87eb9018732a5e11994bd1b7ec/matplotlib-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:3e8576f7c47e02fd4f21f44171302d2d1d58d4471d46da3d71fe8899d19539d9", size = 9028295, upload-time = "2026-09-11T19:03:57.261Z" },
713+ { url = "https://files.pythonhosted.org/packages/54/b4/4facdb700bc236e4c676495ca3798d8134ab8668cbfe0f6b2e9ebf962a38/matplotlib-3.11.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c9721f81275499da1feeb36a2cf8192ea086283b3bd16b7dc4c9d7aedb7396d6", size = 9478976, upload-time = "2026-09-11T19:04:00.12Z" },
714+ { url = "https://files.pythonhosted.org/packages/4c/a0/bba05f0a25bfeb0475c2f2a87958b31103aa6b7a5386017afb1a96212825/matplotlib-3.11.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a3040b209f3968b4e84161df7b174f07a9fad33b0f2d7e48ea3bbd3075e2863", size = 9307932, upload-time = "2026-09-11T19:04:02.976Z" },
715+ { url = "https://files.pythonhosted.org/packages/d4/42/81d5cba4bb39b41b0998efc880e129f2d7cdb94256ca49678fdcd9c21be5/matplotlib-3.11.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3fe71fbfb8ec0e310e0bc8537c3405a01f38f25f9394ed2135e6202fed542b", size = 10674370, upload-time = "2026-09-11T19:04:06.912Z" },
716+ { url = "https://files.pythonhosted.org/packages/29/5e/52f56f93d5b20815ee0ae352f7a38eff25580edb180b4fa7df87cfeba68f/matplotlib-3.11.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c8255de28f986d935a64c9ca71c0ec2d2f41d355691f5ea684725dc91413f71", size = 10954597, upload-time = "2026-09-11T19:04:09.948Z" },
717+ { url = "https://files.pythonhosted.org/packages/6e/ae/f8918a1b564db49c53cd603eb3c8748c5767e4ce0fb2cf1d7c38653e4c5d/matplotlib-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a8756cc73d9af9a7fe0deb54ea2e75ef73b01d9e575877e72acad5458e660943", size = 10812583, upload-time = "2026-09-11T19:04:12.887Z" },
718+ { url = "https://files.pythonhosted.org/packages/ea/61/4315ef0d63937fe56f451887b4a0be3b8e8b22cf1d92a8d157caac54570d/matplotlib-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:ecea603dd2fbf8242fd31a305a8b12a4ece2de28096870c65fdd0d1e35b8d9a6", size = 9505972, upload-time = "2026-09-11T19:04:15.788Z" },
719+ { url = "https://files.pythonhosted.org/packages/d5/00/02398b0a1a62ef73d9af0a957fe1773363eec9a6ca7e433c72ae2790fbce/matplotlib-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:01dc8eaaab5a9fce9ff615eca82345728f289e4715b186ee10c6d85272fc26bb", size = 9184534, upload-time = "2026-09-11T19:04:18.691Z" },
720+ { url = "https://files.pythonhosted.org/packages/ec/c5/1e6ca10baa7e04f113c37856aadb4adc3dbb1c55fc7da6dc830bba1292d7/matplotlib-3.11.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79a258f58253dfa025af80a9e9bb228d75fced007f0e93ae7423fefbde81a74d", size = 9526415, upload-time = "2026-09-11T19:04:21.39Z" },
721+ { url = "https://files.pythonhosted.org/packages/e0/93/7561727af07ccc84747953c6a33eee3981cc436d04f465032e707ca0e429/matplotlib-3.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c0b83f044ce10a98027b105b3931548719a6e8c7ef986b4362651e0b5367c8dc", size = 9361064, upload-time = "2026-09-11T19:04:24.107Z" },
722+ { url = "https://files.pythonhosted.org/packages/c1/40/ce270ef2a6794d94a409fe28b8dabedffc07c07f9e5dbb89e0f605bd157d/matplotlib-3.11.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e43b188f0a5b75447bcc197728258166aa64365770ed1caa36595a5e1ca4bbba", size = 10684626, upload-time = "2026-09-11T19:04:27.201Z" },
723+ { url = "https://files.pythonhosted.org/packages/45/46/396e0307dbf1c1f2d0a3654caa6dbc780dd5379f5f11257369cf8d21d9fe/matplotlib-3.11.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3304eb5a59442a8867f6920d484591c0fa09ffc29e9260be2feec3351e25869", size = 10960602, upload-time = "2026-09-11T19:04:30.254Z" },
724+ { url = "https://files.pythonhosted.org/packages/1c/97/4defb84695477340aa88b27c9482cc1ca4de11f36f66b6c8c0ddd685def1/matplotlib-3.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:02329432ae5c6af87cf208ee575b701d698bdf0b1a3bb28cc6d53c36e967e575", size = 10822603, upload-time = "2026-09-11T19:04:32.964Z" },
725+ { url = "https://files.pythonhosted.org/packages/7d/e7/eab54551c1ec6b72dcd678a078b6c6da8d4c85ec1ff0016e81fe52866304/matplotlib-3.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f2ac30cf5eb5dff1b584627ae0b0e1186551a4f69ae3c75073911da497a29170", size = 9551206, upload-time = "2026-09-11T19:04:35.811Z" },
726+ { url = "https://files.pythonhosted.org/packages/4e/0e/04698032cb4d8fb1e30d8ffec5ded20f6f582b03c8c58b2385c95245f158/matplotlib-3.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:cf41ecd1b0c0b6f7177ed965a54c2afbe888715c7cf6054dc12d53bc1494002c", size = 9234977, upload-time = "2026-09-11T19:04:38.972Z" },
727+ { url = "https://files.pythonhosted.org/packages/44/a9/ca827c184b87364378964e36e7c0ae23651d146fdbc4000635205490cf82/matplotlib-3.11.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:894a9cbbecbe30ae6787d464df2e8fc7a8d475cfc68f87c3029f7c11152899b3", size = 9478997, upload-time = "2026-09-11T19:04:41.914Z" },
728+ { url = "https://files.pythonhosted.org/packages/48/60/01aaafc9c3159f3fae9a65bbcddaa4a6d4d72b0aeffdd636d7f6d00a5c8d/matplotlib-3.11.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:bbf1062991d826ed27e2144f3afa4461afbb8ff56e8f703043e191a8163b1ee9", size = 9307947, upload-time = "2026-09-11T19:04:44.834Z" },
729+ { url = "https://files.pythonhosted.org/packages/1e/a0/6aa72c7cd7cf4dfac652744d7a3310007cec3ce8e5ab9be607bec7096471/matplotlib-3.11.2-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:399fef672f7046ef7d6a57572b2a6f9845f3f5afcff04e7b2df7a363a9f42190", size = 10674370, upload-time = "2026-09-11T19:04:47.745Z" },
730+ { url = "https://files.pythonhosted.org/packages/a1/97/d7e90225ac145046787a2ee0fa4cdc7e4d5cb6772ea232130b10f8a4095a/matplotlib-3.11.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc82dde2a0d3e3ad472edce04897ad7146b8d8bfd1df8a32992eebb81af18fdc", size = 10954581, upload-time = "2026-09-11T19:04:50.712Z" },
731+ { url = "https://files.pythonhosted.org/packages/58/18/89cf056a9d2cae01f4b8baf01c7e809d613244fcdfa2a2acf28c1e0d79fa/matplotlib-3.11.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:930efb28f59fda124e39265d177bab302625297bb147d2910de725a2fc2aef54", size = 10812605, upload-time = "2026-09-11T19:04:53.552Z" },
732+ { url = "https://files.pythonhosted.org/packages/9d/4d/c61b9f4c34af802d4d84e58803e009013d626b5f468bafe1416131863b5b/matplotlib-3.11.2-cp315-cp315-win_amd64.whl", hash = "sha256:3da3bc0cbf7245e7db72cc6d29d12c5abef72cb73059c73b945f14e3545f3eb2", size = 9506068, upload-time = "2026-09-11T19:04:56.519Z" },
733+ { url = "https://files.pythonhosted.org/packages/03/7b/047f2fa8708d47430d8e7fa45e9fffb0aee5d4e300f978e087be2ee11fbc/matplotlib-3.11.2-cp315-cp315-win_arm64.whl", hash = "sha256:f25446b2981717dca9786bac841cb3fd7efb568e3e3c755dd980481c5cb9228d", size = 9184576, upload-time = "2026-09-11T19:04:59.318Z" },
734+ { url = "https://files.pythonhosted.org/packages/03/6e/ad9c5989950785a98642667df265a47c6637e924cb69b5dcf68c3299ae7c/matplotlib-3.11.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fea03cf56568cc1cba08b470be6a0559e71c3a5b688d54b7179bb35ba23d0821", size = 9525988, upload-time = "2026-09-11T19:05:02.304Z" },
735+ { url = "https://files.pythonhosted.org/packages/a6/c3/25c61701fb655dd412428b15eee2591ce554cdd593e39a045a153373f428/matplotlib-3.11.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:254d4ddb2fa8df3b4c689c0c306063aee10521df82cfb438185e499c75fe37c1", size = 9360774, upload-time = "2026-09-11T19:05:05.092Z" },
736+ { url = "https://files.pythonhosted.org/packages/58/5a/4552878fce8f9d249a5e264c16f5c267c27eac08fb039fa3e207a06feff5/matplotlib-3.11.2-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60cf3047a51edecdc4535a9196bbd6a732936b8e9f8184aabf4d16165884aaf", size = 10685892, upload-time = "2026-09-11T19:05:07.686Z" },
737+ { url = "https://files.pythonhosted.org/packages/af/dd/b3b19ec0124fc4aafd8a4b3bae859c562a7ff0fd8ae3b2099947f78bde19/matplotlib-3.11.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3712dc9b464793de0a4e42a7313d50293c751f94bddaf7332a1bc71bccdda9", size = 10964946, upload-time = "2026-09-11T19:05:10.943Z" },
738+ { url = "https://files.pythonhosted.org/packages/7b/30/687ccce22f66a57fed85942450e88062773bcbf1a42914e70c7148ec71dc/matplotlib-3.11.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:75b6d88402770e181b5d06a67dda4129c31da7d05004d63a21c310ec78d1b83c", size = 10824960, upload-time = "2026-09-11T19:05:13.9Z" },
739+ { url = "https://files.pythonhosted.org/packages/d3/34/117fe35a8d05054b4b34d2d29287099927c24c3841d36e06d9fa2f20c1cf/matplotlib-3.11.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a6939df7567114b6bac7f4c5e06c84a67f197c1b2f2e4b234d4eecb3bfec9482", size = 9551204, upload-time = "2026-09-11T19:05:16.48Z" },
740+ { url = "https://files.pythonhosted.org/packages/63/5a/9f440e7bec8b80d0af2a65d1165b2f4e718982dad785531f62255d9ff19b/matplotlib-3.11.2-cp315-cp315t-win_arm64.whl", hash = "sha256:d480038c83691532ed52ff3147db51fa902fc78cb2d8349993a1cdb684435bff", size = 9233946, upload-time = "2026-09-11T19:05:19.063Z" },
741+]
742+
743+[[package]]
744+name = "narwhals"
745+version = "2.28.0"
746+source = { registry = "https://pypi.org/simple" }
747+sdist = { url = "https://files.pythonhosted.org/packages/67/43/e11d75a7fb635c6f63dd6e176cb3e050ec2fee35f2f2267b332a6ad351ec/narwhals-2.28.0.tar.gz", hash = "sha256:2d9860c80d6610820826e94c6e42ce4b1d92d89cdd479586e652a8d211dab345", size = 754977, upload-time = "2026-10-27T12:19:33.777Z" }
748+wheels = [
749+ { url = "https://files.pythonhosted.org/packages/cf/82/8ef9c7e0f50493307a49c09cfa7f00c5a778b6d1921f7b2dabe1d3a5809e/narwhals-2.28.0-py3-none-any.whl", hash = "sha256:1f69d6385ec30e2fa6e1fdeb37c562ecabd0f713f6f3e3b58a668c5c9df9567f", size = 490591, upload-time = "2026-10-27T12:19:31.649Z" },
750+]
751+
752+[[package]]
753+name = "numpy"
754+version = "2.5.4"
755+source = { registry = "https://pypi.org/simple" }
756+sdist = { url = "https://files.pythonhosted.org/packages/95/b0/c7453d0b6e2073c3264468b106ee1563750cecc910965e67357e3698c83e/numpy-2.5.4.tar.gz", hash = "sha256:9a94cf751c9ad8ebaa835bcd3d40dacf8534ad086b88c38029b65123c7999d2a", size = 20866315, upload-time = "2026-10-10T20:05:31.422Z" }
757+wheels = [
758+ { url = "https://files.pythonhosted.org/packages/d0/97/ba2074e92b7befea137e77ea8471e768bbd87c339b7e8c9f5a931949f977/numpy-2.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6342f54c67093cae5c0227eb0eb772fdb79f2a2c37a6eb278b9909ee06aa356", size = 17001609, upload-time = "2026-10-10T20:02:40.843Z" },
759+ { url = "https://files.pythonhosted.org/packages/ff/a9/bac826765e971d8e16e2064e9ac7525fd69b40ac17c905033a7f5442023f/numpy-2.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b11e8fda06a7d69f15ebf542660b74466c2e51094800c1fb794f47ad4faeef17", size = 12015718, upload-time = "2026-10-10T20:02:43.45Z" },
760+ { url = "https://files.pythonhosted.org/packages/31/2f/5ea3570fcb8ccd0882bea99436a513b2c85dad8f774a2057849130a8fb99/numpy-2.5.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9cb18a327b49c5c337f972b03682f6a49855525faaf3c0d3e9c96cd0fd8880a8", size = 5451717, upload-time = "2026-10-10T20:02:46.169Z" },
761+ { url = "https://files.pythonhosted.org/packages/34/f2/b4fc1bafca03868220b5eaf729d2f21ebd7d7b151c0f9e144fe212bbca35/numpy-2.5.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:aec3fc4b32ff82421274f5d205c559c51c840c8df66a78efd7f3612dd005a26a", size = 6789926, upload-time = "2026-10-10T20:02:48.139Z" },
762+ { url = "https://files.pythonhosted.org/packages/dc/96/8319e2457ae4333c62c815c7006b869a4f60985c1e01024c2f8c6c040fe5/numpy-2.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4d21ab149f15e4e6043dfb0de87e6e5f34ac176cde83060e9802981fca2ac2", size = 15695312, upload-time = "2026-10-10T20:02:50.115Z" },
763+ { url = "https://files.pythonhosted.org/packages/43/a3/c799c62e19c337e6d3770b08e475887fb30ce8477d3c09efca6b2f0228a6/numpy-2.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbde6962867ee75b48b0ee29b2b9372ec5d617799dbaf38e82dc0596f2f7738a", size = 16727283, upload-time = "2026-10-10T20:02:53.186Z" },
764+ { url = "https://files.pythonhosted.org/packages/39/6b/3604e53fb00314d0dc1b94ec9125a1484f649c0a17480b1f0f0c7a9d6250/numpy-2.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:381a7a3d2e65e64c0ec302795ab9dc12bb1e73f150904699c153716177eebdaf", size = 17047890, upload-time = "2026-10-10T20:02:56.038Z" },
765+ { url = "https://files.pythonhosted.org/packages/4a/7a/e8b58a5289a0d464c52885de47c35a935cdd70c03a4c3ab94a5126416dd0/numpy-2.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b89d0aaae2fe498c648f4c4795c084db535af5bd98ef942b2a3681fb74ce8645", size = 18485839, upload-time = "2026-10-10T20:02:59.018Z" },
766+ { url = "https://files.pythonhosted.org/packages/6f/c9/47094f597015009f310b8c900def59065ef1ff5a6fe7b51fc65ec58ec2c6/numpy-2.5.4-cp312-cp312-win32.whl", hash = "sha256:9968ab7e49b93ac6e1c3b2239732183152c9150f16308d30b66a372cffe3483c", size = 6138936, upload-time = "2026-10-10T20:03:01.626Z" },
767+ { url = "https://files.pythonhosted.org/packages/12/33/fefe62073dc8acfd0f2b9ed7c003af2f50aa61555e113e6db02b8f79f145/numpy-2.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7b1b6353e36a7e50de2973a38d705c88ee93adcf120673cee7f45a4a3fa223a", size = 12573091, upload-time = "2026-10-10T20:03:04.349Z" },
768+ { url = "https://files.pythonhosted.org/packages/1a/07/161270b0c2eec56e4c905f6d6d22e1b836887b2cb189d3f5820aa588e9dd/numpy-2.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:aa1cce2ff3f8d953de38b76bf44602caeb69f101430208f64a10067f7cb4b1d3", size = 10521630, upload-time = "2026-10-10T20:03:06.767Z" },
769+ { url = "https://files.pythonhosted.org/packages/67/14/1c3ee0118a8fce08565a5d8482631608426a33af10a01077fada5dc7c119/numpy-2.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2377da2dd3ba2c1200956acbab2a358c83b8e1f8531191672d1cd6ad83250d53", size = 16997729, upload-time = "2026-10-10T20:03:09.291Z" },
770+ { url = "https://files.pythonhosted.org/packages/83/8c/b0ea9477fb1f0d4484bbc5cba21678cc9969704d8d7f3f158d1db35f8e14/numpy-2.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7415db95818b39ec475a5eea54d9e3b6bc83e3912158e46da3438cdce399804d", size = 12009826, upload-time = "2026-10-10T20:03:11.946Z" },
771+ { url = "https://files.pythonhosted.org/packages/e2/84/6a3d75b3ba3dfe84ac0053450753d1e6d250a8bf80f66474cc46d1fb643f/numpy-2.5.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6d6a71b9d9a97c03633aa12565ef2825ffa036cc1d99cfd50dacf0f128af4fe2", size = 5445803, upload-time = "2026-10-10T20:03:14.329Z" },
772+ { url = "https://files.pythonhosted.org/packages/61/18/bb993f267ca20b376e07092a16793a5b31ed3138751e9ba480011a14d742/numpy-2.5.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d8200f16437b289a5bb927c6e184eccc3e8389bc0070fea4cd5b9e13c1757959", size = 6786220, upload-time = "2026-10-10T20:03:16.602Z" },
773+ { url = "https://files.pythonhosted.org/packages/db/b6/135bb0953b61dc21c6cafa14b424ae666944e4899cf140e00c2b322a1a45/numpy-2.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2e71b04c6cad90026e544501bbe0ab9290fa8a4d845e7e8c0d124fb429c988", size = 15689178, upload-time = "2026-10-10T20:03:18.721Z" },
774+ { url = "https://files.pythonhosted.org/packages/da/24/3bd070f3269dc609d8f26b2643f62ef91bb415841c0b294805aaf7fe06da/numpy-2.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffa07666f8da0eef81d149934a626d0d95fbd6838432a33e66245423a9062c0", size = 16718044, upload-time = "2026-10-10T20:03:21.386Z" },
775+ { url = "https://files.pythonhosted.org/packages/c7/8e/9d15bd356b0a019c965312b1a3c6a727cac4cae5bc40045fbc12ce4cff9c/numpy-2.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fa3328f784fc8277fc48026f6cad516f5c561c5d8e2e39b3c9e0c8f23223b34", size = 17048364, upload-time = "2026-10-10T20:03:24.468Z" },
776+ { url = "https://files.pythonhosted.org/packages/dc/fe/9d5b560db964f15871885f2250795d15945f8699e17ef90c0c2ff4c875b2/numpy-2.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b86966fbe4ad7de710422175572bcdc75fdedadfb54bc6fab7deabccddd7780b", size = 18474904, upload-time = "2026-10-10T20:03:27.895Z" },
777+ { url = "https://files.pythonhosted.org/packages/e9/98/d27552990f1bd611ef3e7466adadc78312ea2df63b83aad47fdc3d3ca8df/numpy-2.5.4-cp313-cp313-win32.whl", hash = "sha256:5258bc06526964be5face2fc6f756857a3f24f21ec3e72ca131337a75b165d6c", size = 6134537, upload-time = "2026-10-10T20:03:30.511Z" },
778+ { url = "https://files.pythonhosted.org/packages/90/8c/140a40398a66b4471211be1affdb6ed24c486d581bd28d07b7f2fcb69540/numpy-2.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:8b4d2fd2d34e5f8c9235ee787de5631a37a28402b15cb80814df973d2be54129", size = 12566113, upload-time = "2026-10-10T20:03:32.612Z" },
779+ { url = "https://files.pythonhosted.org/packages/34/52/01d205e5e8ccb27b2b0b141e801f22b830198c979111b0fa44771438d9a9/numpy-2.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:bc39ac66a7a9a3fbd6134fda43136b60ffde99c8f4501e64e0d2b24da137babf", size = 10519523, upload-time = "2026-10-10T20:03:35.163Z" },
780+ { url = "https://files.pythonhosted.org/packages/99/ba/005cb5edd580d2f84d7ca3206b92dc17d4388e56e6f87ffe8f2762f83139/numpy-2.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c668b2f0d651605b58892644b0e302c7157f7159544227758c896982ef384b18", size = 17005499, upload-time = "2026-10-10T20:03:37.961Z" },
781+ { url = "https://files.pythonhosted.org/packages/f3/49/fee7587c33ee35f7977f9051d7f2023d4e7246d62710c80f20c2361ea232/numpy-2.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffa6ce09a1c6a08e9667dd9c97aa0b14184e8d18f2a14b78b2a2328c9147f076", size = 12019666, upload-time = "2026-10-10T20:03:40.606Z" },
782+ { url = "https://files.pythonhosted.org/packages/d5/b2/c6ce165acffceb15a82c07b9cc77d391f86b3f379ba62911908ae5d34b91/numpy-2.5.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:956555e0603a4d38019ae6925711cb9dc43195c076a928accf7ea5d50bddfe53", size = 5455617, upload-time = "2026-10-10T20:03:43.138Z" },
783+ { url = "https://files.pythonhosted.org/packages/77/7f/dd85ce260a669a89be06842cf355d7353a33e6cfbc590fb8ebb947d88dc9/numpy-2.5.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2c2c4afffdeb7920e445028dd71eb932cac3e704792e964bc2a232426d4f1255", size = 6791932, upload-time = "2026-10-10T20:03:44.874Z" },
784+ { url = "https://files.pythonhosted.org/packages/63/d6/34b0a2b0741386a63025a65a2c09caaaaaad6d0ca95b66cd65c30dd7fcb5/numpy-2.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4054173604cd8658796053f1f3bc0befb68ec1c0762c57fdad61e199256a8617", size = 15710899, upload-time = "2026-10-10T20:03:46.839Z" },
785+ { url = "https://files.pythonhosted.org/packages/16/d5/928078d2b28f26829b138b4a6c3980045022fb409f570657a224ae60ef4e/numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d549420b8858885cea8838a727842249218b9c1da24dd517e25c9c7a948310a3", size = 16721710, upload-time = "2026-10-10T20:03:49.489Z" },
786+ { url = "https://files.pythonhosted.org/packages/f9/cf/673fd1b8f4cd78eb6320e87ec4c90ac19c095644259e3749853a405c70f4/numpy-2.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:823874a507a84af050493b622affde94b6f7c3a0dc22cb2801381bc03b871c00", size = 17066182, upload-time = "2026-10-10T20:03:52.25Z" },
787+ { url = "https://files.pythonhosted.org/packages/f3/92/a77b5061b1b3e2643928c37976d79ee173e1b171ed158b7a3c61056b41bc/numpy-2.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4e263278bfb5ee6409db8aedbc4cc32973b1b82bc1e8d3c668551d04d83a7e37", size = 18480315, upload-time = "2026-10-10T20:03:55.39Z" },
788+ { url = "https://files.pythonhosted.org/packages/bb/1d/1486ef3d3fb2279fd93c4c43c1bbbf1ca389a19816696684409f71babaab/numpy-2.5.4-cp314-cp314-win32.whl", hash = "sha256:cfd73180400042a7c532d30c5e287bdd03c59ff9ee1b4c0316af0539e29dfe23", size = 6185739, upload-time = "2026-10-10T20:03:58.186Z" },
789+ { url = "https://files.pythonhosted.org/packages/52/9a/e1e512ebc948d5b9dd33b08736760f0ebbed2848fd4eda1f553088a6dcee/numpy-2.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:2ca144f15135b6212a5c47b1e2aeca6e412f102f95a2d5d88d8aec77eb255de3", size = 12703552, upload-time = "2026-10-10T20:04:00.28Z" },
790+ { url = "https://files.pythonhosted.org/packages/2c/05/de709a982d7bbcd688a3fad71f002e9ff80c2db39e03ee726609b610f1d1/numpy-2.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:468397ba3c64427474706e5c9123fe266395496714dc684294eac75cd4930d1e", size = 10803901, upload-time = "2026-10-10T20:04:02.659Z" },
791+ { url = "https://files.pythonhosted.org/packages/13/34/083570ada3bb2a30fbe5d77c8c6fef9141144a15d33e6f793a67e9749ab8/numpy-2.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1ef3aa6d7e29bb13677323114280b05acc57607fa2300e66432d665d5418a162", size = 12138695, upload-time = "2026-10-10T20:04:05.012Z" },
792+ { url = "https://files.pythonhosted.org/packages/94/06/1f9c24db48eef0c2d1207e3b11fffb0478e39dfd8c1e1be7476936885eed/numpy-2.5.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:98b053943e5a0474ec0da309d2cb9d3f18ea57f8a2067c2ab7b5f763d1068380", size = 5574615, upload-time = "2026-10-10T20:04:07.316Z" },
793+ { url = "https://files.pythonhosted.org/packages/da/0f/593fba2e1560e949123bc7d2fc48b5893d56e58cd4bd5a273d2fbf60b220/numpy-2.5.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:b64a85f40e154983960a4167d4c1d57a50c7f109b3d3264a3a984154e90a8454", size = 6889383, upload-time = "2026-10-10T20:04:09.918Z" },
794+ { url = "https://files.pythonhosted.org/packages/eb/9f/b799dfdce4e05e80ed4bc815c71ff343a11533b2c0ffc221cae8538cda63/numpy-2.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a813ed7719bf45463c51779e6a98d0385fe905e48447526938a4b8337333d551", size = 15753763, upload-time = "2026-10-10T20:04:12.278Z" },
795+ { url = "https://files.pythonhosted.org/packages/34/88/16c5f12f86f5ad2817c4d103205131fc6c8acb3d1878af05a1a4f23ec859/numpy-2.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b80cdf5cedba0e90d93fa5f9a333c4d65bd545cd669b71bb97ce2b703c9d73", size = 16757212, upload-time = "2026-10-10T20:04:14.799Z" },
796+ { url = "https://files.pythonhosted.org/packages/ff/4f/a1fe40e18a898e6a5089f4f0d891f0a493eb0574d5b34458f0fbe5aa3e5c/numpy-2.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2199ed071f460487c8db2c0e5c0b564494190edb4772fe80f9aad88b2604def5", size = 17116471, upload-time = "2026-10-10T20:04:17.58Z" },
797+ { url = "https://files.pythonhosted.org/packages/aa/46/e923a11c78e65c1722e7aaad817c06bd591324174b9d28ce5d31eee4d432/numpy-2.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:64f9c9878c1938476365e11ccfb6b770f3b9e5f045ccddc514235041e6959365", size = 18524063, upload-time = "2026-10-10T20:04:20.365Z" },
798+ { url = "https://files.pythonhosted.org/packages/5a/fa/84ab064514440c1f64a1b21088f2c82756defdd05e07c75ab233899565b2/numpy-2.5.4-cp314-cp314t-win32.whl", hash = "sha256:64d1c8ac28a4077cf987e0a71a7a0ef7e2df70722f07f0baa42dbb7eb6938647", size = 6340926, upload-time = "2026-10-10T20:04:22.865Z" },
799+ { url = "https://files.pythonhosted.org/packages/7e/7e/6cd886876f435b10685db9b9f7eeb70356f99e052116f4e5f11c5792c714/numpy-2.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:067374eb538c34c745436365cf7b0112595c1d326f21ce4ff340f61230239fbb", size = 12901584, upload-time = "2026-10-10T20:04:24.99Z" },
800+ { url = "https://files.pythonhosted.org/packages/38/1b/3c1684f6a06f7307f2335fca6e486cb162847fb97e91d65f8eb5cabad213/numpy-2.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:e94aef2c639da4a960ad0db8e06471208d8589974953d78b61d345b4eb99e394", size = 10891152, upload-time = "2026-10-10T20:04:27.52Z" },
801+ { url = "https://files.pythonhosted.org/packages/08/f4/3224deff3af2bef6bc0b175369698d8cb348f3d91d9bb0286cd5c9eae9e0/numpy-2.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8dddfbee2e68d26d0d7d7d9cb247b1fd4409241cce32d815a11d97ec2cfde179", size = 17003231, upload-time = "2026-10-10T20:04:30.021Z" },
802+ { url = "https://files.pythonhosted.org/packages/be/75/fee0b8c6d94b44b2fdfae74f6a4ad5a138739589a8aebaec28ce4e713ed5/numpy-2.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:81e3420b27048b65eb14c3acf0c174a8cb0e023277716110347d2dcb26026dad", size = 12018300, upload-time = "2026-10-10T20:04:32.519Z" },
803+ { url = "https://files.pythonhosted.org/packages/47/c0/d0b335a499a04b65f532c3f034346ef390f81299060f928492dabc1e0272/numpy-2.5.4-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:0b4724a19de67bea8cfc4970798efa78bcbbe2ac2613cfac16721a42d44de2a5", size = 5454250, upload-time = "2026-10-10T20:04:34.943Z" },
804+ { url = "https://files.pythonhosted.org/packages/5a/0e/461b3783c03d668052e6a21b01b673db6ffcb7831fd32d9aa5368c1cd426/numpy-2.5.4-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:2132418bf8dd124a427ca9e6a1daf9ee1a87185344c95119ceae868b99466da1", size = 6789644, upload-time = "2026-10-10T20:04:37.258Z" },
805+ { url = "https://files.pythonhosted.org/packages/b3/02/5dad269b02166965a7b4ca14adaddd75dbee0de42435bfecf561b84ba5a6/numpy-2.5.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:325518d4245b9e331387702aa58c2ce1dc4cdcbb41dfb4ccd5dcbc7e08db1266", size = 15704353, upload-time = "2026-10-10T20:04:39.616Z" },
806+ { url = "https://files.pythonhosted.org/packages/93/3a/01360c8036822ed9f7aa32189a77d1476567ec1e8e1383522389e4faac45/numpy-2.5.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56733449d2544178beaa4545cee357370440cf056c197f9c7bfb19dbfdd0e86d", size = 16718648, upload-time = "2026-10-10T20:04:42.383Z" },
807+ { url = "https://files.pythonhosted.org/packages/7d/5c/b863a2c093c4d6f21a597fcaf24ead0835c09ab16a8312d5a5a8868af683/numpy-2.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5ec3753760c1a6d8bb91200666e545c3a9728e6269dfb5d6ce02340996698aa3", size = 17059053, upload-time = "2026-10-10T20:04:44.976Z" },
808+ { url = "https://files.pythonhosted.org/packages/0a/60/ced4f57f9a1258a0af74f17cb0b0c2700b5c67cd6678823c803b263e4df3/numpy-2.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b1185012870173de7ae33d370bd45b1cf5baee747ea4b97036b65f4e93016877", size = 18477406, upload-time = "2026-10-10T20:04:47.863Z" },
809+ { url = "https://files.pythonhosted.org/packages/f9/bd/0ef22dafaafcc7d4bb3ca26b8d2afbd55dedad8eaba99a8c864e1997456f/numpy-2.5.4-cp315-cp315-win32.whl", hash = "sha256:298eca75243f2cbbfdb460560b9fb2a1792a33cf2ab4286efd43d92e8d3df508", size = 6185133, upload-time = "2026-10-10T20:04:50.467Z" },
810+ { url = "https://files.pythonhosted.org/packages/50/bc/d2651b155ecc608a77e6f4d15495c11f14f19bb98f8bf0c5b0d38f86dda1/numpy-2.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:332f3378fe077dd850e677ec01bdcc4f22368fb5d50ef10b2c79230b1bf5a592", size = 12703085, upload-time = "2026-10-10T20:04:52.63Z" },
811+ { url = "https://files.pythonhosted.org/packages/dc/d2/45e404f8abb26fb9eda12b94012936873e827b1be76f2ee7890be128312e/numpy-2.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:d4cccbbc78717966f764cd3af4fb70276fa01fc7a2688af11c78901fa5c04f05", size = 10801451, upload-time = "2026-10-10T20:04:55.677Z" },
812+ { url = "https://files.pythonhosted.org/packages/c6/c3/2ae14e09cfdb67dc187a342e15308a21c15bf4d2071f8079e6aee5fe56dc/numpy-2.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:950ea81d57ef070665581b6e1b5f6a029306423cd1739c5b95fe78aa30db6b9d", size = 17097121, upload-time = "2026-10-10T20:04:58.403Z" },
813+ { url = "https://files.pythonhosted.org/packages/f5/cf/305ae624ef8a039414317224abe9ec9c2fe7ea3c2e1cf204d43ff6b2ffb9/numpy-2.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c05ede731b03fb1b7591faca9389ade3267d2bddf1ad8882bb3f2cc5e101694f", size = 12135439, upload-time = "2026-10-10T20:05:01.65Z" },
814+ { url = "https://files.pythonhosted.org/packages/a9/a8/f75c63813aef95827bb2c0d13b12803016853056e8792c280058cdbfe783/numpy-2.5.4-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:5fbf7141bbfd63aea22f435c9062a032b9ea0082fe9845dad7f021d3f1234e71", size = 5571451, upload-time = "2026-10-10T20:05:04.135Z" },
815+ { url = "https://files.pythonhosted.org/packages/6f/0f/f17763f983868b5c49b4101ebd7e00760bd1769478a6bb6a8de6e085bbac/numpy-2.5.4-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:3573cd22564692a5b899ec344e5d5b9cc4576f2985b96f22af3564ed54f2710f", size = 6883356, upload-time = "2026-10-10T20:05:06.249Z" },
816+ { url = "https://files.pythonhosted.org/packages/67/a7/8af04c5a79e047996cfa38854dcfbececdd0343a7c933a46fdd03ef6f5da/numpy-2.5.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c109eac9cd439193678f69d70733c1108487546ca8eafc107b510ae10c1aecd", size = 15750991, upload-time = "2026-10-10T20:05:08.376Z" },
817+ { url = "https://files.pythonhosted.org/packages/57/7a/648254290d0c504faa8f2d07aa206660c728802c781a6f3fc68ab7cb5d71/numpy-2.5.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80d6ef6e8620eb2c2b4c4caad50b5935d6db3cde2d51581b55dcc79e14016d1d", size = 16757675, upload-time = "2026-10-10T20:05:11.393Z" },
818+ { url = "https://files.pythonhosted.org/packages/b8/fe/4a8c3cdb0c70400cfe4c5bec42d3099a5673802a95064614b33e07b82aa1/numpy-2.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:77045a4b175bbf5316ec08003880804336c78f92281a1b72222b274ea85ec5ac", size = 17113846, upload-time = "2026-10-10T20:05:14.49Z" },
819+ { url = "https://files.pythonhosted.org/packages/1b/7e/619692bb67778702c0e9eb2d468568a7573f4e269386ea61aed01ee4e557/numpy-2.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0f02a46e49cfb6c73bdb7aea1c0d3461dbae9aba613542b65f657cd3d17b9fab", size = 18522915, upload-time = "2026-10-10T20:05:17.33Z" },
820+ { url = "https://files.pythonhosted.org/packages/b7/b5/4da41c328788f575838f97a098fe8ca691ebc6f6fd73ad4a262ee40b184d/numpy-2.5.4-cp315-cp315t-win32.whl", hash = "sha256:ad62a416ddcf863bf44bba76fbf6b53366ab0692e294f51cae4b5fbe0d246788", size = 6335804, upload-time = "2026-10-10T20:05:19.921Z" },
821+ { url = "https://files.pythonhosted.org/packages/98/94/6482ddfa3d312490cb9358f375bf2ad56427dbea8769187158e94d653753/numpy-2.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:38f47be9f74ab870d2633b5456ae519c43758a8d1fd05342f0ce4ecc034396ee", size = 12890095, upload-time = "2026-10-10T20:05:21.875Z" },
822+ { url = "https://files.pythonhosted.org/packages/48/7f/c2d1b436b6e7cfebac140c2579a298344b85f2991a2ce5c3615cefb29400/numpy-2.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:7a14a461d9340f1b46b8648578aed9cdb8b3b018a8fac6c1dde2c9192a01a87f", size = 10883718, upload-time = "2026-10-10T20:05:28.547Z" },
823+]
824+
825+[[package]]
826+name = "openai"
827+version = "3.40.0"
828+source = { registry = "https://pypi.org/simple" }
829+dependencies = [
830+ { name = "anyio" },
831+ { name = "httpx2" },
832+ { name = "jiter" },
833+ { name = "pydantic" },
834+ { name = "sniffio" },
835+ { name = "typing-extensions" },
836+]
837+sdist = { url = "https://files.pythonhosted.org/packages/31/ce/5b2e20d41c4e82dbb4be0192c62ce53516111896f642466fa71b82dccf0f/openai-3.40.0.tar.gz", hash = "sha256:df309d434130e89a23e9c687fc4bcd3df0df705a33d3872503c87448df69f56f", size = 2391443, upload-time = "2026-10-27T15:38:54.916Z" }
838+wheels = [
839+ { url = "https://files.pythonhosted.org/packages/1c/d2/b59435562e893730bb620c720ddaaa84b016e870c9b7b6b85fb505540bdc/openai-3.40.0-py3-none-any.whl", hash = "sha256:dfcfd4f65ee5a8bb824da90c9ad7fa0dda7f3bd472a7c564dffc6183f0a0b55c", size = 2353063, upload-time = "2026-10-27T15:38:52.679Z" },
840+]
841+
842+[[package]]
843+name = "opentelemetry-api"
844+version = "1.45.1"
845+source = { registry = "https://pypi.org/simple" }
846+dependencies = [
847+ { name = "typing-extensions" },
848+]
849+sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
850+wheels = [
851+ { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
852+]
853+
854+[[package]]
855+name = "packaging"
856+version = "26.3"
857+source = { registry = "https://pypi.org/simple" }
858+sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
859+wheels = [
860+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
861+]
862+
863+[[package]]
864+name = "pandas"
865+version = "3.1.0"
866+source = { registry = "https://pypi.org/simple" }
867+dependencies = [
868+ { name = "numpy" },
869+ { name = "python-dateutil" },
870+ { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
871+]
872+sdist = { url = "https://files.pythonhosted.org/packages/55/b1/a10c0d1a7e3ce44eb42635771004aa503e3b9ddde0bd39c524dda65f7eff/pandas-3.1.0.tar.gz", hash = "sha256:ecb9b2d8a817b1e1ba1fdb90c0abe9eab31b4fdf2562b7a56b86ac66ace17e49", size = 5933421, upload-time = "2026-10-22T11:57:03.087Z" }
873+wheels = [
874+ { url = "https://files.pythonhosted.org/packages/e9/12/b94c28870f351e9650c724e6daedf661455b348a9dca6d717c42c30fa216/pandas-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4772320b7735d96dbbef437129f840dec4175f8a18a2391bd1222ed8943825c", size = 11587014, upload-time = "2026-10-22T11:55:07.208Z" },
875+ { url = "https://files.pythonhosted.org/packages/1b/4b/af8ca15261db52ab07f5d0c2d4c1bc28d822ea4c2a79756bc2606c012e7c/pandas-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc5e4e12d287ea77bbf4c4d4bf47ed152d82a990d3a6794dee7c350f887532b5", size = 11167712, upload-time = "2026-10-22T11:55:09.523Z" },
876+ { url = "https://files.pythonhosted.org/packages/49/c4/cd5127751e2bef3d10f07e0d856e120916a3df0df6450f759ddb48c78b1c/pandas-3.1.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b4e696ff70ed06e986feeec09356c491ce878d0df69a36c8e591b6a38afa29b", size = 11476964, upload-time = "2026-10-22T11:55:11.889Z" },
877+ { url = "https://files.pythonhosted.org/packages/c5/a6/dc89cdd95e12093bc813e9b814a09f1a056be3e1304edaef9bf9dbee108a/pandas-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9433b5dc2f26ef9d9b56daaf4d41d078bf4372c5d88c42ed5b397a6eea80de16", size = 12019688, upload-time = "2026-10-22T11:55:14.246Z" },
878+ { url = "https://files.pythonhosted.org/packages/36/87/da14aa671da792612f15847451e731d5dc0f06f6747b76b15f197ffcce13/pandas-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cba20fd269808e224d6212273008173978033a1dee0f3f217bfdbd58775a72c", size = 12490149, upload-time = "2026-10-22T11:55:16.675Z" },
879+ { url = "https://files.pythonhosted.org/packages/5e/a0/1ce996c7354ad37983955571058ba0dde585663163ab067275197183868f/pandas-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:743554ed96755ad1963397ccadbe9965d92db8777ca2ae8c1fe58bd9549d7f28", size = 13090624, upload-time = "2026-10-22T11:55:18.854Z" },
880+ { url = "https://files.pythonhosted.org/packages/92/72/d235f82b51a22478938a3583b68e8dc05811937543adf410f34bab76004a/pandas-3.1.0-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:ac2f833557874a79fc0b5fac4c86e402ea6498beba8753dc29ca23d08481689a", size = 8091628, upload-time = "2026-10-22T11:55:21.285Z" },
881+ { url = "https://files.pythonhosted.org/packages/10/72/06aefff8fe5efc5e1ca8bfb2695b648a435358f6dc224d4e40159314181a/pandas-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:b8c75dbb27c7d170b25c13dd35515da4bb92d40fe8bb3bd2c69a95de2218889b", size = 10748836, upload-time = "2026-10-22T11:55:23.47Z" },
882+ { url = "https://files.pythonhosted.org/packages/f6/b6/4c86f659f5b14790d14e17253becc93c4e3d1511b73f0ba2184f06b164f8/pandas-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:73b6cea22090b7123c78ee662a5108ff36616b0bec4fe47026f86fb04a49bce7", size = 10128898, upload-time = "2026-10-22T11:55:25.995Z" },
883+ { url = "https://files.pythonhosted.org/packages/01/54/eb85f8b2ce121c21286842cf81abaa670ec3570173c512298f91128927a5/pandas-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:849d5019dead65944875ce21fbf71a2ca18e982164cd7367623cbda8567c3122", size = 11609893, upload-time = "2026-10-22T11:55:28.592Z" },
884+ { url = "https://files.pythonhosted.org/packages/9b/f2/b327e814327b7e7e61b5c5cceafd5c0c20f0d38e94f99ebf2dd6c1cec85b/pandas-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c1e084760f3b275eed5863407d025b933d25f3247515fcc84bf664405e8bb3d2", size = 11182834, upload-time = "2026-10-22T11:55:30.912Z" },
885+ { url = "https://files.pythonhosted.org/packages/4e/6d/2b918a45ad31d85f898567c948f11d0789ffe7fbadd6f487565e99d179b6/pandas-3.1.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1009f5b6819b81319cf177b4f01a2823ed54c81db8bd11cdf2375f42d064c02", size = 11421478, upload-time = "2026-10-22T11:55:33.257Z" },
886+ { url = "https://files.pythonhosted.org/packages/4b/6f/b63e1d43eef5746014ae3f4786c72e0020e61b6b373a2abb6ebf1be3df49/pandas-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de6d0fc96e8551b83f400bf212611a0b3a2b389594d20a2540dacc61b21e7bce", size = 11991052, upload-time = "2026-10-22T11:55:35.556Z" },
887+ { url = "https://files.pythonhosted.org/packages/fd/43/2cf85145d822957a3f8959fd0a850e6bf687dac2823c612b3912b8888dae/pandas-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5611344f9f0ed51619ac7acdbf72b86f5043801a66e3f8730c8971257082e8ad", size = 12455560, upload-time = "2026-10-22T11:55:37.656Z" },
888+ { url = "https://files.pythonhosted.org/packages/7f/72/0cb19cea2e6afbb0854cbde0ed1345b61600e0de84eb7b73583fde3e3b55/pandas-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:39c67ed9e56ad3ac0727dc2ae6a1eef766988c762d5ce2e2a162613d81c8990b", size = 13045764, upload-time = "2026-10-22T11:55:39.969Z" },
889+ { url = "https://files.pythonhosted.org/packages/e3/5c/ea5472e8444f5871427f3a6818725684fcdce74223ca1b22aa7ca345d490/pandas-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:07807035ad66de0d833d68f9bee05801dbf08b88d56a233b0e9fd189372c3261", size = 10729719, upload-time = "2026-10-22T11:55:42.008Z" },
890+ { url = "https://files.pythonhosted.org/packages/45/f7/8cdd350c67bc62865d1e30cd7add43dd631420223487ba42dfcfd0d0e052/pandas-3.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:54fed4a50b8321ff8d413b46b7e31a023cecefd06c7210c02aa980a9836428b2", size = 10116651, upload-time = "2026-10-22T11:55:44.367Z" },
891+ { url = "https://files.pythonhosted.org/packages/c3/a9/e82d44fe20dc61dd2e15bdd17c78aa0023d860d55620e4fe82f9e644a089/pandas-3.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a8c44d40cbb6d00128bf2d8f8bb66c8d561744b6d44686b5a1a01f3b3fd1245f", size = 11675868, upload-time = "2026-10-22T11:55:46.617Z" },
892+ { url = "https://files.pythonhosted.org/packages/83/38/6be618d98e2cbbb99a40349717532f0c2b6794ddc62f1199e3f83e45a40f/pandas-3.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89b66823678cdd53cf97187e7cd6861402bdd147d11a383706093136a4e83f45", size = 11262553, upload-time = "2026-10-22T11:55:49.143Z" },
893+ { url = "https://files.pythonhosted.org/packages/06/5c/66e27fb0d691e835bd55b336bdf47312a541a66a17ba4d72a988d743bb79/pandas-3.1.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a8dc7780720b9e2182799ee86279d4cc7d1fd30b230ea1b202da5b6f7e14c3a", size = 11548155, upload-time = "2026-10-22T11:55:51.674Z" },
894+ { url = "https://files.pythonhosted.org/packages/37/1e/42e81484c5a6791ab1b8c52c1ff1ef0483c119f710ed58ee603d241b8792/pandas-3.1.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4455cdcc03e42c17233184e8e4ea65c3ed2925d8b1d9d7dd8e6fced3f6e43c1", size = 12029136, upload-time = "2026-10-22T11:55:53.823Z" },
895+ { url = "https://files.pythonhosted.org/packages/cd/c9/a3b4fd156457f3eb2585838cd56427a113eeeba679204de435a3224b8cc3/pandas-3.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:224a9267e5fa70c16bf4f8a317435eef98387940ce6c8c611f1f68f2c3415e10", size = 12575350, upload-time = "2026-10-22T11:55:56.006Z" },
896+ { url = "https://files.pythonhosted.org/packages/4e/25/23a017003d792117b297bbd87e35a1bb5658509f610ecd5aeaebb84762aa/pandas-3.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c261c7a808982557c2917d40ffd27916769f26aacb4e6775c01e4467bcd34c3b", size = 13096142, upload-time = "2026-10-22T11:55:58.434Z" },
897+ { url = "https://files.pythonhosted.org/packages/1c/d2/f4074b7d1234d112aaa23a796be1574441a9fa873707eb98312a237a6185/pandas-3.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:e77112cfcabb3bef7281a72170670db9a5b31409a1cd3199939abb1fba67a7ad", size = 10885958, upload-time = "2026-10-22T11:56:00.895Z" },
898+ { url = "https://files.pythonhosted.org/packages/25/9f/bcfc9e0ea07f5c7583e129e62cd0529747317977ac1b85878b80235bb322/pandas-3.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:6c51273b89784c9d54020ed2f60e20524169aef349bddf6cba9f5e83b46077a7", size = 10281257, upload-time = "2026-10-22T11:56:03.478Z" },
899+ { url = "https://files.pythonhosted.org/packages/05/e4/b3bf0b702c0a886e4abcfc2ebdc681310498592a10d9ca0ce5db6df47b70/pandas-3.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84377fa82d0b40165379a0058d8a1a267ceb09e65160de6fcc75475d4d595884", size = 12134028, upload-time = "2026-10-22T11:56:05.696Z" },
900+ { url = "https://files.pythonhosted.org/packages/3e/ff/9e08cf048d73f4dfdb10da859790cd1a4213465dec39ea367a606d580c4a/pandas-3.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:37f88576f3449bfd86c1052f6b42274b61d7250c8d9dcc55f9dc4de7fe9883ae", size = 11729164, upload-time = "2026-10-22T11:56:08.335Z" },
901+ { url = "https://files.pythonhosted.org/packages/1f/ff/a0df60d70fac10006841457c30d35647ae0c93af33e420e5e047b15e058d/pandas-3.1.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:756fd67995f3efb6b25f8eaea95c0e893f84e0068e2c53c5fdbb65275041d4db", size = 11413041, upload-time = "2026-10-22T11:56:11.38Z" },
902+ { url = "https://files.pythonhosted.org/packages/55/04/e6742d85ba7d166a1a8a99ff9f4aa9e585fb24028522daa187859ce50b5d/pandas-3.1.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4d9252b57bded194d09cd5ebf18d9b4edac238b16f56984ea8281d2ecfe39cd", size = 11869427, upload-time = "2026-10-22T11:56:13.637Z" },
903+ { url = "https://files.pythonhosted.org/packages/0a/a1/c9f4915408ef10b541f31b7812ca038dd903572c163b7be7bdcece8a5110/pandas-3.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:461b79db058639e446ddf90dd7f4804fae385acf0669f6e31321333e14cb6bc5", size = 12430367, upload-time = "2026-10-22T11:56:15.829Z" },
904+ { url = "https://files.pythonhosted.org/packages/9f/ed/636b9cc8d30b3e2027b43036dd5a8e9e7cfdd5aa86f4454235c5ef7f96db/pandas-3.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bfac077874a396b7ae61a307b27aa6f0256cf31c143b7f96ac622765754cc300", size = 12945204, upload-time = "2026-10-22T11:56:18.419Z" },
905+ { url = "https://files.pythonhosted.org/packages/e6/5e/5c28942fa8c6cf2d8b4e941abee43a44ba0a123f9fef6411f0319aefddd5/pandas-3.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1340f823a1d189fae8365de314c4d3c2ea0173c61bca66cc76a91385c64d9d3d", size = 11297553, upload-time = "2026-10-22T11:56:20.934Z" },
906+ { url = "https://files.pythonhosted.org/packages/cd/a4/5991dfb2cccd233ae754f2fb37a4b1474d90595d83f6790467dc5a7d8041/pandas-3.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2bb3f8cc42fae0c0de5186778f4a4957656b9b3955aca48383abfdac44a83598", size = 10626068, upload-time = "2026-10-22T11:56:23.187Z" },
907+ { url = "https://files.pythonhosted.org/packages/77/26/5cd42609d8e6cb3991242116a0e499d09f29f5bcf574f72f0a4aba13a87e/pandas-3.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:04ca88727dfa86837030c493d7512bb1c008aa4f5f14149c5f8d3ff446791340", size = 11666949, upload-time = "2026-10-22T11:56:25.842Z" },
908+ { url = "https://files.pythonhosted.org/packages/97/5f/ca1ac99da700c215c52fb0f5063a7fbf14878c6e100f687704c21985640e/pandas-3.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:c0bfec4d188fb0c05bba47b9f9def87624b8c545913aa3585a28f19f27ce847e", size = 11255062, upload-time = "2026-10-22T11:56:28.201Z" },
909+ { url = "https://files.pythonhosted.org/packages/d3/eb/8d7aef062e7d1f48869288e5c5ae5d109923f37ed3bbd0fdcaecc87784e9/pandas-3.1.0-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfb087db83eb79ca348047961d367bdd4bf9820d90a5945f3652c7060da7363", size = 11547558, upload-time = "2026-10-22T11:56:30.492Z" },
910+ { url = "https://files.pythonhosted.org/packages/06/c4/a041c786bc29935afe44806f72c0111f2643cbac5cab761007a227e12e97/pandas-3.1.0-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b5ce18612ca955e7a95d0bacc20120aedf97e2fbbebbe4de029fc24479e79b8", size = 12026259, upload-time = "2026-10-22T11:56:33.219Z" },
911+ { url = "https://files.pythonhosted.org/packages/d3/4a/ce6690c7e6081c96857ad7dcbe3b978394d8db682610755919e61863516f/pandas-3.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a13199463b129363701a540c7b79586581b246b10b6a7ad682c41b268e7991d5", size = 12563821, upload-time = "2026-10-22T11:56:35.402Z" },
912+ { url = "https://files.pythonhosted.org/packages/75/ec/ae440ecbfa8f5ba0230541b4519c1fed0c4293ff362f18f7f353b12e2dca/pandas-3.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8e4de3fe8017e3445f1174707cd977e4e1154a704af2c060a1f648862f81acb9", size = 13086426, upload-time = "2026-10-22T11:56:37.666Z" },
913+ { url = "https://files.pythonhosted.org/packages/e7/1e/6459d4b0ed9c3d72b9ab63387ec11e1e658579e702e0ef117be8b5d7d0d1/pandas-3.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:eee72bba8a72f0bf9522132e5c87cd10fce9d77440b50f65326b093ec9fb8d43", size = 10877850, upload-time = "2026-10-22T11:56:39.994Z" },
914+ { url = "https://files.pythonhosted.org/packages/f5/22/e5fad1828e5f923e80a8d7ba9187f687159004a10f99f95dd5346b04c54e/pandas-3.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:9ea2983fe096a3d997c8c4b9f26775fa51e7fb251b10634bd7f87e6fe3542d52", size = 10276384, upload-time = "2026-10-22T11:56:42.193Z" },
915+ { url = "https://files.pythonhosted.org/packages/59/76/89820c2e1950425f39d30695d2296a8c7d400c4c2d279e7618363a7289ac/pandas-3.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:84d73e0814a38711e4f2a24b272f748a0b4335169d2f9752407ae26bde22990f", size = 12107257, upload-time = "2026-10-22T11:56:44.498Z" },
916+ { url = "https://files.pythonhosted.org/packages/c8/2c/af4397e6e855d2c0dcb3392ab7788087be4ef53f6a3f166daad8b83c783e/pandas-3.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d3920d9b4de89fbef76ba6b863d58a330b5bfb720c9f8f58eb9b7da9b711d034", size = 11704430, upload-time = "2026-10-22T11:56:46.791Z" },
917+ { url = "https://files.pythonhosted.org/packages/fd/9e/7791cd269099f9fca7dc4718955cfe4bb94aedb6634b59eeffefb1ed6a74/pandas-3.1.0-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1d61e0eae5219cd024fcbc21304de759964b5642ddcdbb28d9a8ba442ab6901", size = 11378442, upload-time = "2026-10-22T11:56:49.113Z" },
918+ { url = "https://files.pythonhosted.org/packages/54/6c/da4013bd33c49a12eb3127771111caeed3222a985651e3465bc06f839ce7/pandas-3.1.0-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbb121205af7bc5e587b94a07b5f67b5ba687a918665b9e8748f21388a88b044", size = 11850193, upload-time = "2026-10-22T11:56:51.558Z" },
919+ { url = "https://files.pythonhosted.org/packages/14/89/8c820bd6fb6ad8f0edb2877639bc7e164bfb4b8e4cdf7a2202f54959ea1b/pandas-3.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:684ef3add5a92abb99c1622f2bd04b54b8843075a266e8240106ca94ce1d1237", size = 12394022, upload-time = "2026-10-22T11:56:53.785Z" },
920+ { url = "https://files.pythonhosted.org/packages/92/f5/9d753d85c4ea85b6d1c3735496ae5b416d7ef6905f1845c70e9521d124d9/pandas-3.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dde9a55ecd3c4f9100ac50ab15e852e4c54fcf372d800e4495e0b2159a0122ab", size = 12916085, upload-time = "2026-10-22T11:56:56.084Z" },
921+ { url = "https://files.pythonhosted.org/packages/e6/f9/4a6965677477dbbb19f8881befb01b67eb6009809db73f7f1b7a0132faf1/pandas-3.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:3fe234bfb9999552dc54c45851752e56e163218d7663897dca950b4a7c32af39", size = 11267097, upload-time = "2026-10-22T11:56:58.479Z" },
922+ { url = "https://files.pythonhosted.org/packages/99/35/e60fd8228db4eb848802988dbc6e3a0f1a81c57f06ad95ebe4cd534fa655/pandas-3.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f1a93c24175129bfc9922ef748ddae1d8137c743ed835e2615be329f606fed4d", size = 10597861, upload-time = "2026-10-22T11:57:00.776Z" },
923+]
924+
925+[[package]]
926+name = "patsy"
927+version = "1.0.3"
928+source = { registry = "https://pypi.org/simple" }
929+dependencies = [
930+ { name = "numpy" },
931+ { name = "packaging" },
932+]
933+sdist = { url = "https://files.pythonhosted.org/packages/08/e3/950e513affdfe1c1864397b4f40d6ed06ec1b54f608e462c27f151dd72df/patsy-1.0.3.tar.gz", hash = "sha256:79ebf4c93ff4d296e58a9d5be2b2ee31bd49d737cf11d70ffbd8a44b2de42e65", size = 399992, upload-time = "2026-08-29T21:35:32.258Z" }
934+wheels = [
935+ { url = "https://files.pythonhosted.org/packages/20/70/cd3cf5cff538323076a879edef02cce6f13f7d75e03d12f211b0a0dbd178/patsy-1.0.3-py2.py3-none-any.whl", hash = "sha256:d3dbebe8fd5f46e29912d030b63c6268647b59bf788a99e2af28a30234cf357c", size = 233318, upload-time = "2026-08-29T21:35:30.913Z" },
936+]
937+
938+[[package]]
939+name = "pillow"
940+version = "13.0.0"
941+source = { registry = "https://pypi.org/simple" }
942+sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" }
943+wheels = [
944+ { url = "https://files.pythonhosted.org/packages/7a/b8/58d79332229482eb0d27c82973850808c19d37e5bdc14443d3417bd373f5/pillow-13.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:67c8d44ad89fa7eb88de5d6ef6199b1e03dd24974f85a67639fc8badb801fe79", size = 5568643, upload-time = "2026-10-16T22:15:44.474Z" },
945+ { url = "https://files.pythonhosted.org/packages/39/4b/f2ed53d83aa44ac19599ac0a54897f867fafb95f406980d1e24083f8cbe3/pillow-13.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc481f742ccda20e9ed760b8f978dff50f2a4e9bc253ac167fb0a05b0783ef52", size = 4852329, upload-time = "2026-10-16T22:15:46.284Z" },
946+ { url = "https://files.pythonhosted.org/packages/3b/bb/9e30e8ed18c20800ee327a12107d5249ad8dc444867b3adfcc21654e3c71/pillow-13.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bf295fe459343bc6093c8e5e0df60bfa189e0fcb775a22e2c2488fdeccc6c0d", size = 6519309, upload-time = "2026-10-16T22:15:48.14Z" },
947+ { url = "https://files.pythonhosted.org/packages/e6/99/fdf65cc539e266ab93546c261bdf98411fe165c6e4e13f9dbf597097192b/pillow-13.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42d63be3af4f9b951b199b0a9d23365d37ab14687e6d77b74940432c8515c4ca", size = 7144542, upload-time = "2026-10-16T22:15:50.093Z" },
948+ { url = "https://files.pythonhosted.org/packages/26/9b/f38b7c4d8f599537e2d1a6d546cec9beef7536ff8f5c49a3f9c78179d8ac/pillow-13.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bd024d567eca931b668978d672dba914d4b8e2628e2c3073a2ad178de8a2620", size = 6618042, upload-time = "2026-10-16T22:15:52.054Z" },
949+ { url = "https://files.pythonhosted.org/packages/a8/7c/45ee28531f89ee4582967d8a17f7a26d71ca64cd902b93c269590cd60a00/pillow-13.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23edf4ec4b6f4f240e8df6e4985e9ee2ced86474632433e55fb1e2548744bf40", size = 7228137, upload-time = "2026-10-16T22:15:54.009Z" },
950+ { url = "https://files.pythonhosted.org/packages/01/bc/9c2a969cb1b70201d04fa17fd5aedf02a732870b8da86bceaaa30b0ca385/pillow-13.0.0-cp312-cp312-win32.whl", hash = "sha256:02464da86f561f410e13b061ec37c3068a9cb557a095494157742d07b3c24a27", size = 6515423, upload-time = "2026-10-16T22:15:56.25Z" },
951+ { url = "https://files.pythonhosted.org/packages/ff/cd/73a79ecdc822076002eb24b5e1e4a530b0bb5310fcb6d239abbec877110b/pillow-13.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ff3fe9eb621fbb9db4c4be33ab946f6bac5f98ab50ca8d3260503653656c961b", size = 7272166, upload-time = "2026-10-16T22:15:58.597Z" },
952+ { url = "https://files.pythonhosted.org/packages/4a/a3/be5a1236efd085b3984e4305da8f179ae9dab80c991d2ad78f2bd5c4c843/pillow-13.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3bc8ee0271387e0b1e3ad0ff893645990a656e79bf9c42f4f734a3642b11d51", size = 2698943, upload-time = "2026-10-16T22:16:00.637Z" },
953+ { url = "https://files.pythonhosted.org/packages/82/e2/9132d38df320e8099e2642d9fa538f18a7dfebf68a875a31725dc2a115aa/pillow-13.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00", size = 4190620, upload-time = "2026-10-16T22:16:02.527Z" },
954+ { url = "https://files.pythonhosted.org/packages/cb/be/436dac0996876f65ef1916ffb711c5ef4af2ebc57414b6f16642e07f6b47/pillow-13.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033", size = 4290286, upload-time = "2026-10-16T22:16:04.861Z" },
955+ { url = "https://files.pythonhosted.org/packages/60/16/7c1007162e4169ea0d5bec47e2e0b2174f56c50cf5b8c02a1dc23e41037e/pillow-13.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599", size = 3723169, upload-time = "2026-10-16T22:16:06.956Z" },
956+ { url = "https://files.pythonhosted.org/packages/08/bc/578ab2eaa04705a8978bd9be0fe15c3d43a8fc95097c4f7e39540e79e4fa/pillow-13.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe", size = 5568469, upload-time = "2026-10-16T22:16:08.664Z" },
957+ { url = "https://files.pythonhosted.org/packages/98/c8/ccede3dcc4f274b80fb49199eed1ef34ba05566c8b71976b9d1f111da2e3/pillow-13.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313", size = 4852187, upload-time = "2026-10-16T22:16:10.693Z" },
958+ { url = "https://files.pythonhosted.org/packages/fb/3c/cd0770e96d20bfd990db024bcbd43897ff3e88da1f4782cf90ea231b1d4f/pillow-13.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369", size = 6515908, upload-time = "2026-10-16T22:16:12.896Z" },
959+ { url = "https://files.pythonhosted.org/packages/ef/fe/f5cdaa2d1c4086e757430f368c271a58ee29cfe7378e7ae7b9d964015c25/pillow-13.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582", size = 7142544, upload-time = "2026-10-16T22:16:14.844Z" },
960+ { url = "https://files.pythonhosted.org/packages/fb/b5/a54f6d44adbcd23737c8c12953f42be107e4cca64e247c0ca3a5b563061c/pillow-13.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81", size = 6613097, upload-time = "2026-10-16T22:16:16.893Z" },
961+ { url = "https://files.pythonhosted.org/packages/e6/b4/9e6c1207c21b2e91db0d15c16d30f3701ce5add1bd4da0e787761e66cb6e/pillow-13.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502", size = 7224846, upload-time = "2026-10-16T22:16:19.195Z" },
962+ { url = "https://files.pythonhosted.org/packages/78/9f/d3c3cb0518f3dd1eee0ef501535f53d15597a53c5c4118d178086143397a/pillow-13.0.0-cp313-cp313-win32.whl", hash = "sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8", size = 6516013, upload-time = "2026-10-16T22:16:21.389Z" },
963+ { url = "https://files.pythonhosted.org/packages/d7/ed/1b16fb66a37e1b9e41370b93b2c614c7da761a5b2a41dc333f7af4639962/pillow-13.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42", size = 7284777, upload-time = "2026-10-16T22:16:23.393Z" },
964+ { url = "https://files.pythonhosted.org/packages/87/7d/8d02eba9f7ecc87f8e90f731e032cc4773802f2a843763afd0c66a535ff4/pillow-13.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f", size = 2698826, upload-time = "2026-10-16T22:16:25.382Z" },
965+ { url = "https://files.pythonhosted.org/packages/8e/45/fbad9c7f5e2f1745c373a434d46072bc84eddfefaa28b852425ac957fda6/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", size = 4190721, upload-time = "2026-10-16T22:16:27.524Z" },
966+ { url = "https://files.pythonhosted.org/packages/a2/bf/2074902a259a90692c5dbf132885d21671644cbbd3f690047516243a55db/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", size = 4290236, upload-time = "2026-10-16T22:16:29.562Z" },
967+ { url = "https://files.pythonhosted.org/packages/ff/a5/3d1bb93b8ea38d15d21356c1b8e9f50971ac592aaa248f7fb71739094c0e/pillow-13.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", size = 3723093, upload-time = "2026-10-16T22:16:31.249Z" },
968+ { url = "https://files.pythonhosted.org/packages/84/a1/be060135d85187b25fa8b2573786c0e45c62ae27527fff5257675eb97f4a/pillow-13.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4", size = 5572253, upload-time = "2026-10-16T22:16:33.103Z" },
969+ { url = "https://files.pythonhosted.org/packages/b5/f4/0bf6f1ac9ba7ee2ffe9914aeb410a1035a0c1a3c42f122388d7fc950bc4b/pillow-13.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d", size = 4852269, upload-time = "2026-10-16T22:16:35.102Z" },
970+ { url = "https://files.pythonhosted.org/packages/83/16/b603274617b3e6728da3a0133d4fba1ea8d7fbe0c4ea5fef9cbffa398b8b/pillow-13.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", size = 6516058, upload-time = "2026-10-16T22:16:37.228Z" },
971+ { url = "https://files.pythonhosted.org/packages/1d/f2/1371707ce438ba9a43f9d24089828494ec6f9396e28cd7903aa704dd6306/pillow-13.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", size = 7142574, upload-time = "2026-10-16T22:16:39.172Z" },
972+ { url = "https://files.pythonhosted.org/packages/b9/a9/62cf68587f910bc87c5951bcb6e9430c7643a65076adf5f18b4bf350e2bd/pillow-13.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", size = 6614295, upload-time = "2026-10-16T22:16:41.117Z" },
973+ { url = "https://files.pythonhosted.org/packages/6c/c9/9e9b48178649730299be0bafbf5f810a5b6977538f5a92d47e4ba5f5d608/pillow-13.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", size = 7224841, upload-time = "2026-10-16T22:16:43.118Z" },
974+ { url = "https://files.pythonhosted.org/packages/8f/48/c44e91384b1b6c072a714a63d00608acc16a6a968735e478e4be4384e5ac/pillow-13.0.0-cp314-cp314-win32.whl", hash = "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", size = 6508694, upload-time = "2026-10-16T22:16:45.883Z" },
975+ { url = "https://files.pythonhosted.org/packages/50/e2/7ad17747517e4c813dca78257d6bb593466f8231baf1384bc98d92745edb/pillow-13.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", size = 7281830, upload-time = "2026-10-16T22:16:47.725Z" },
976+ { url = "https://files.pythonhosted.org/packages/41/9f/e5b51f2ce8ef4563215334035403ce61a244f93a2e65c24a940131618f87/pillow-13.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", size = 2695468, upload-time = "2026-10-16T22:16:49.515Z" },
977+ { url = "https://files.pythonhosted.org/packages/97/73/a53502f731f6c84805a4ce87a8881e86d07a65aee64728b900d112dc703e/pillow-13.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa", size = 5578196, upload-time = "2026-10-16T22:16:51.291Z" },
978+ { url = "https://files.pythonhosted.org/packages/21/50/7e949959a56b5559c78ef172b70fc6025706acb289f3f648a15329e4fa6f/pillow-13.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f", size = 4857276, upload-time = "2026-10-16T22:16:53.399Z" },
979+ { url = "https://files.pythonhosted.org/packages/c9/52/43ded0ac08e77e39d17bd6d894162572201b5e8fdc4a022b14c55a1960a4/pillow-13.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", size = 6607008, upload-time = "2026-10-16T22:16:55.525Z" },
980+ { url = "https://files.pythonhosted.org/packages/0d/2e/4a57ed216d5bf877387cba4d44c02aa87abe51ad2e06404aa6b597c4da39/pillow-13.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", size = 7224281, upload-time = "2026-10-16T22:16:57.738Z" },
981+ { url = "https://files.pythonhosted.org/packages/5c/3d/2929b248e135752d63aaeabf53f6de797e905b7b6263f9c1e2adade20aba/pillow-13.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", size = 6695600, upload-time = "2026-10-16T22:16:59.842Z" },
982+ { url = "https://files.pythonhosted.org/packages/bc/33/fb0da992cf084ed1ee6ee05e96b6191e35c351eb707ca2df874feb24ce47/pillow-13.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", size = 7300589, upload-time = "2026-10-16T22:17:01.686Z" },
983+ { url = "https://files.pythonhosted.org/packages/bb/af/c30dd9ef6a3dce84ecb1b2393cac629047a32e66279056c6cb46c3ffd65c/pillow-13.0.0-cp314-cp314t-win32.whl", hash = "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", size = 6517204, upload-time = "2026-10-16T22:17:03.722Z" },
984+ { url = "https://files.pythonhosted.org/packages/85/d5/ef3a8ae7609c8ab26570abf18194882534b480d245a9148b895a2262c004/pillow-13.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", size = 7286695, upload-time = "2026-10-16T22:17:05.543Z" },
985+ { url = "https://files.pythonhosted.org/packages/84/3c/0861db0c69a03689ebe3e7599d35668a414deb5aaafd8255215e4e62e3d3/pillow-13.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", size = 2698098, upload-time = "2026-10-16T22:17:07.674Z" },
986+ { url = "https://files.pythonhosted.org/packages/6e/eb/e86a41962a3a31ad495a3d6a912d9002b01bba4a6fa71c28703abdca7f69/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521", size = 4191213, upload-time = "2026-10-16T22:17:09.449Z" },
987+ { url = "https://files.pythonhosted.org/packages/dd/04/7e7445e706afec84ef06dc25b2c77f9cecff242c23730c62928ad1089d4a/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc", size = 4290650, upload-time = "2026-10-16T22:17:11.196Z" },
988+ { url = "https://files.pythonhosted.org/packages/46/85/d534089068ac40c0f61f650389a296c812eab745a8a733f81feec39ae955/pillow-13.0.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac", size = 3723078, upload-time = "2026-10-16T22:17:12.772Z" },
989+ { url = "https://files.pythonhosted.org/packages/ce/2b/00b5c3b401df538b5dbe311d3899730fae5ba1e21cb8b80a128887ac6747/pillow-13.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d", size = 5572292, upload-time = "2026-10-16T22:17:14.539Z" },
990+ { url = "https://files.pythonhosted.org/packages/72/f5/a6b0610f946e9b54ae888ba7ad29d6e2f8d9b11f69e212d70416b7186c09/pillow-13.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782", size = 4852222, upload-time = "2026-10-16T22:17:16.521Z" },
991+ { url = "https://files.pythonhosted.org/packages/1e/d0/c0ff25bb0427ec70f32aac995453321fc3c4494a46c7292ff02d9d260b04/pillow-13.0.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071", size = 6527349, upload-time = "2026-10-16T22:17:18.534Z" },
992+ { url = "https://files.pythonhosted.org/packages/85/11/e46fa0bcc0fc117f1eacc206b3d51f04d583fab328b4b740672d0438d83f/pillow-13.0.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf", size = 7152873, upload-time = "2026-10-16T22:17:20.415Z" },
993+ { url = "https://files.pythonhosted.org/packages/7c/53/2bdeca705f3a58679fa9201233193c86a30238340ca29b3a152c3fcc719b/pillow-13.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3", size = 6626871, upload-time = "2026-10-16T22:17:22.361Z" },
994+ { url = "https://files.pythonhosted.org/packages/14/de/3829030f209722ade3753bb456b805a0e7f885cf885b483407365c82e9ca/pillow-13.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932", size = 7236439, upload-time = "2026-10-16T22:17:24.352Z" },
995+ { url = "https://files.pythonhosted.org/packages/44/b1/67fc37388382bc036a74f04da5bb6fcbf90ec3ac1f45893e06176c373fd3/pillow-13.0.0-cp315-cp315-win32.whl", hash = "sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81", size = 6508688, upload-time = "2026-10-16T22:17:26.314Z" },
996+ { url = "https://files.pythonhosted.org/packages/44/05/fdb6a79aee10861165526b41b7b440e36ee1ff58613b2fbefe3acbf06d4f/pillow-13.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6", size = 7281107, upload-time = "2026-10-16T22:17:28.198Z" },
997+ { url = "https://files.pythonhosted.org/packages/e6/9c/6c6f7dc2a566467baf121394e83de25ac287cffe34cc030726beb4be0224/pillow-13.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0", size = 2695513, upload-time = "2026-10-16T22:17:30.013Z" },
998+ { url = "https://files.pythonhosted.org/packages/23/16/e9120defdb8bff693b582d035312acc6f530cdc2bc5238a87f410efccd82/pillow-13.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f", size = 5578129, upload-time = "2026-10-16T22:17:31.642Z" },
999+ { url = "https://files.pythonhosted.org/packages/47/22/76faf2bb2448f48a84cf894c1e2bb1090d7d2b1aa521ece131cb2f05d631/pillow-13.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88", size = 4857428, upload-time = "2026-10-16T22:17:33.43Z" },
1000+ { url = "https://files.pythonhosted.org/packages/b3/64/0861ea4e03c50b14af9ed9dd8b9f388ab42f445c623a9fcbab19aef7aa55/pillow-13.0.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb", size = 6612582, upload-time = "2026-10-16T22:17:35.415Z" },
1001+ { url = "https://files.pythonhosted.org/packages/5c/20/cd1e9b3f60a9f3d5ebe46da36955c26004092a501f7f2c42f9fcc26611bc/pillow-13.0.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe", size = 7230176, upload-time = "2026-10-16T22:17:37.362Z" },
1002+ { url = "https://files.pythonhosted.org/packages/b6/ef/ffb0a2fc85690714fb7ac662956949645d57f15bf4426f520da361e7b6e3/pillow-13.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22", size = 6703640, upload-time = "2026-10-16T22:17:39.313Z" },
1003+ { url = "https://files.pythonhosted.org/packages/e0/c2/6402ee3ccb3363aeed8fdb3f9fe56703ac5377a539f0b81b53999d7e121f/pillow-13.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630", size = 7309231, upload-time = "2026-10-16T22:17:41.021Z" },
1004+ { url = "https://files.pythonhosted.org/packages/fc/96/d6b82e8b8c06ad143b66b12e0b31051e4bf489b4fabca1f390a14fe1ebde/pillow-13.0.0-cp315-cp315t-win32.whl", hash = "sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2", size = 6517200, upload-time = "2026-10-16T22:17:42.886Z" },
1005+ { url = "https://files.pythonhosted.org/packages/60/d4/0062e6005e9b270b165015aafd343364ac378ec5a685431ea42baad33357/pillow-13.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43", size = 7287488, upload-time = "2026-10-16T22:17:45.56Z" },
1006+ { url = "https://files.pythonhosted.org/packages/e4/81/b8eb349746be7bce47dd95337c6634e84910af91b59aaa9159c034c614fa/pillow-13.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8", size = 2698073, upload-time = "2026-10-16T22:17:47.556Z" },
1007+ { url = "https://files.pythonhosted.org/packages/60/da/f6d644e631c5b370c5d0b899f3ccadd2780aab070b29a110527cf4e1fd2c/pillow-13.0.0-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:8619685cad8efc4c38c66d3b36aad854523297c43d748b3b6f0cc03c90ba3463", size = 5523188, upload-time = "2026-10-16T22:17:59.334Z" },
1008+ { url = "https://files.pythonhosted.org/packages/e4/b0/2594f202d743a35d4fe80682bfd96f277e5ca7aecb27cf64ef5bfc1d000b/pillow-13.0.0-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:7e91e97a929232cdb828889ee6ad0e48d7f40f4ec38818f4e4a8968b2b1af22e", size = 4806808, upload-time = "2026-10-16T22:18:01.188Z" },
1009+ { url = "https://files.pythonhosted.org/packages/8e/ed/d504165ef79af6fafe25c76726cf30f544cc236002f7ed054f1d51adc4ff/pillow-13.0.0-pp312-pypy312_pp80-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93ff9c9c3663ce23aaa5d0b6b6abac55359e2a0c06c7e30f3771829af816be63", size = 5464358, upload-time = "2026-10-16T22:18:02.779Z" },
1010+ { url = "https://files.pythonhosted.org/packages/7b/fe/a10aba5503ca13e9e009b8bd0a1a4ce5584bdce246245fd6f3a83701369c/pillow-13.0.0-pp312-pypy312_pp80-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e65dd6e75de67b2362a4fa3e16aa627595f65054dc4ec73d64680c84d592fa61", size = 6043514, upload-time = "2026-10-16T22:18:04.538Z" },
1011+ { url = "https://files.pythonhosted.org/packages/c0/37/66b12dc476041fe523bca29c9d58fbf1ea92a4eaf73acfb9e993948f1191/pillow-13.0.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a", size = 7366030, upload-time = "2026-10-16T22:18:06.638Z" },
1012+]
1013+
1014+[[package]]
1015+name = "pluggy"
1016+version = "1.7.0"
1017+source = { registry = "https://pypi.org/simple" }
1018+sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
1019+wheels = [
1020+ { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
1021+]
1022+
1023+[[package]]
1024+name = "pyarrow"
1025+version = "26.0.0"
1026+source = { registry = "https://pypi.org/simple" }
1027+sdist = { url = "https://files.pythonhosted.org/packages/ec/34/17c34cb38e5d940e38f0f0d9fdfa0e8a506676409ea9b85aff7e3079f831/pyarrow-26.0.0.tar.gz", hash = "sha256:0cccd36e00ea3afeb52ded61f2721ce71f604853d70c45365c58324eb773d6ae", size = 1239433, upload-time = "2026-10-09T08:26:25.315Z" }
1028+wheels = [
1029+ { url = "https://files.pythonhosted.org/packages/b3/60/6793778f2617cce469383dac0ba08c4f2401cf342df0c7b9ca53939d9b46/pyarrow-26.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:90ddaf7c625307ad52f31a9b25c34fe5e4897c7529ee3481135822b2b6842ff1", size = 36333953, upload-time = "2026-10-09T08:14:00.387Z" },
1030+ { url = "https://files.pythonhosted.org/packages/db/81/f944cc63ce8a753e5fbff25de6d1d475ebd7fffdf9cf98c65130294fc896/pyarrow-26.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee341973f78a0b46e073d065e88e75026a9c584051e97f98a0d05d96c6bac7dd", size = 38688456, upload-time = "2026-10-09T08:14:04.344Z" },
1031+ { url = "https://files.pythonhosted.org/packages/f5/2d/7e5c722fa5d5d9f3b75e62fe11694b34217664d4f05ac88031197166b277/pyarrow-26.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01c863a18bd9c8412453dd0d92de6d0ee7b2b3d6fb079d9734a4b2a3c8bd4453", size = 50867603, upload-time = "2026-10-09T08:14:09.115Z" },
1032+ { url = "https://files.pythonhosted.org/packages/88/e4/9cd356d906e71bd79b0c3fc5c9a54e01a0020dcf14c152ccfbcb503c7298/pyarrow-26.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6a628922ba20705fa964ca73e4ef959c2fb2f14b9bbec5589a6a1e68e6257c85", size = 53931932, upload-time = "2026-10-09T08:14:24.051Z" },
1033+ { url = "https://files.pythonhosted.org/packages/bb/e4/5bae3133b7fe04c24907a20f3bc1fba388cbbde659199e7b76445982047a/pyarrow-26.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:954d971b363b16ee41f89389a4053315dc71265f2ce5c2468eb0a910b1166268", size = 54444720, upload-time = "2026-10-09T08:14:31.214Z" },
1034+ { url = "https://files.pythonhosted.org/packages/ba/b4/ee422493bb6dafdbef776cfe2c2a73106a1063a79bf4e78d1e5f51176885/pyarrow-26.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5d5768d03426abe6526d5274adefa00abf00a7f81118c46e98b5a46390f5549e", size = 57388949, upload-time = "2026-10-09T08:14:38.964Z" },
1035+ { url = "https://files.pythonhosted.org/packages/54/3c/1783aab1dac28e175dcf26dfc7123725efc474caecaed91e8a34cb89cad0/pyarrow-26.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cc903e1069e9dd5e9dcf780324c0112e27e051e422ecfaff574fb33ed65d9160", size = 28567581, upload-time = "2026-10-09T08:14:44.279Z" },
1036+ { url = "https://files.pythonhosted.org/packages/4d/35/ca95493712af97c46a312945c8e9d16b21c5fe2f148be5466168d0290505/pyarrow-26.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a6ca849f90cf73fe361f08a5762c783ead9671e4548c1f558cc637b54c9103f2", size = 36336700, upload-time = "2026-10-09T08:14:51.399Z" },
1037+ { url = "https://files.pythonhosted.org/packages/69/ef/b1a675f79c9babfd4fcd99af62141d3c2d1a78a524e311b0c6b80110445a/pyarrow-26.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:c2ba350957076b1b3a22f549261dc3e9c67ca20816d8bd5f79d7b9c69be4c4c2", size = 38698502, upload-time = "2026-10-09T08:14:57.114Z" },
1038+ { url = "https://files.pythonhosted.org/packages/3b/7c/cea852a832a327a8de797b3a68e5c25ce0f5aa1d20503807671bd90ec642/pyarrow-26.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e3b190ba1d3d22a5a8758597f797111b77d433473744352a184a5ee0a42d672e", size = 50865064, upload-time = "2026-10-09T08:20:01.614Z" },
1039+ { url = "https://files.pythonhosted.org/packages/4f/d6/e95834b29360092376fe4da9956ba41bb7b021869efe6ee9d4172d05cb15/pyarrow-26.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:240bd18a7487f8767616a948a69dd4e740a8bc36a1c9da49e4dc9a32c5c2faed", size = 53926722, upload-time = "2026-10-09T08:23:10.829Z" },
1040+ { url = "https://files.pythonhosted.org/packages/e0/7f/98257444e2aea2e1fddceee3af3bd2077236d550428413f80393bd1f888d/pyarrow-26.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2b5fcd69c0e1107b79e55839877db5a6ed04651b73fd6fec581d09e230bed5e4", size = 54443093, upload-time = "2026-10-09T08:23:16.971Z" },
1041+ { url = "https://files.pythonhosted.org/packages/88/ca/dac99cfb25cfa62bf7194600cc99abc14a6bd2af50d7fdb7f15eeaf6e202/pyarrow-26.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f7444ea6975c49a857c68f9bd8fa11acae96dede63d120ffb3bf0a603ea82516", size = 57381937, upload-time = "2026-10-09T08:23:24.95Z" },
1042+ { url = "https://files.pythonhosted.org/packages/c0/ed/138d29fddaf803b90f4527e124bb6aaddc18aaf4a6c50fd0a5f577c94989/pyarrow-26.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:3de30a7432b48b98b9decbd9e25a53bb9251d202c2e6c5a29a50869592ccb117", size = 28478571, upload-time = "2026-10-09T08:23:30.535Z" },
1043+ { url = "https://files.pythonhosted.org/packages/8c/32/01858422a37f083911c2bb4d15cc32c5eeaa9d9b2bf5ddedee995a7146a6/pyarrow-26.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5780d487ff6c6ed7b42298609680d87fe0036e529a9dc2e1105364bce9697f50", size = 36378402, upload-time = "2026-10-09T08:23:36.537Z" },
1044+ { url = "https://files.pythonhosted.org/packages/00/85/f6b5976c2878b752d0804d371684e0495a71de296b6dc6559e6fbaa4311a/pyarrow-26.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:a0e4e92eeb088f1d7c2c04d6c7de8434c75abb4b4ccf0bbcd045aa7164c68d93", size = 38733074, upload-time = "2026-10-09T08:23:42.873Z" },
1045+ { url = "https://files.pythonhosted.org/packages/81/bc/c90fcbbcf893631e23dab1b0fb3fa29a508a8614326571b03c0894eda00b/pyarrow-26.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:eaf9e7cc7ab59f6c760232bbde18f64d559bbc50544841303bfb32be53533297", size = 50929201, upload-time = "2026-10-09T08:23:50.507Z" },
1046+ { url = "https://files.pythonhosted.org/packages/ec/c1/0c1ff38ab7df1b2cf54cf0ad9f19a516c4e416c6c9b4c966cc2c9d587f77/pyarrow-26.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ab6914db225d7f399652ae1f08588dfbc9efe617612715701e3d9d5cfa5ca19f", size = 53951865, upload-time = "2026-10-09T08:23:57.692Z" },
1047+ { url = "https://files.pythonhosted.org/packages/9f/70/6a6b170496925472adad45a32528770fc8632db35fc60d4edd1e9ce1be0b/pyarrow-26.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41dd3661ef40790a78870052ad7a58ad827b27c67a4511f06962eb9e9b74d19b", size = 54496388, upload-time = "2026-10-09T08:24:05.23Z" },
1048+ { url = "https://files.pythonhosted.org/packages/a8/32/033ef9dba80976820190e292a10a5a23e9406572b76bbeb4d685d90e5c8d/pyarrow-26.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6e949744dcfc2d379808f7013c5f9cafaf0f817656dff7d46c6931528dd1784b", size = 57411588, upload-time = "2026-10-09T08:24:12.043Z" },
1049+ { url = "https://files.pythonhosted.org/packages/1e/ff/a74892c50aaf1f9f744a84493e08a2f99221e77c39d2d4a926de21a99edf/pyarrow-26.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:4a5fa8dc70dd50808990ff36faf44088e357b353d86c7682dd92d4b78d4c97d5", size = 29237858, upload-time = "2026-10-09T08:24:58.106Z" },
1050+ { url = "https://files.pythonhosted.org/packages/03/10/f0ee0976ef08a851a743c57608917ac9a47623f688b9ee0efe5429975ba1/pyarrow-26.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:e2a1856e9565fe2679863b372478c681806aebbf7d0a6e72f33e77f804e647d6", size = 36495870, upload-time = "2026-10-09T08:24:16.479Z" },
1051+ { url = "https://files.pythonhosted.org/packages/27/ca/0bc431a509bf10b4472dbb94f4184752ecbbddeb7f467152dac0fdaed469/pyarrow-26.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:4bcba83299cb2b8f8e443d36c6ba6269a5034431879015fb0719495df8a14de2", size = 38819754, upload-time = "2026-10-09T08:24:20.875Z" },
1052+ { url = "https://files.pythonhosted.org/packages/61/59/2be41d26af7a07fb71581fb753cae396403ba1a2978355fd553929d44a9a/pyarrow-26.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a4d235876f14b4136b4d616ec42eb469ea0d6ead336cae631aa1dd29b21c962", size = 50933671, upload-time = "2026-10-09T08:24:27.199Z" },
1053+ { url = "https://files.pythonhosted.org/packages/4b/cb/b6d5048cf3178be9678f5c9c60040199894b2f69c3439c87ced91fd24da9/pyarrow-26.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:210cc9b83888b87cdc8f793eebb264f22b20d0dedbedefc73b9687a7047b4747", size = 53906419, upload-time = "2026-10-09T08:24:33.536Z" },
1054+ { url = "https://files.pythonhosted.org/packages/09/2b/23e30fbd776c81d18d134d2592eb60daca13e8a57ab087d0fa042f9d9f3d/pyarrow-26.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca77c43ca55bfc9a4eeb1f0cd5f093f08731b77c24cdba0829035f084959b0bb", size = 54527960, upload-time = "2026-10-09T08:24:41.292Z" },
1055+ { url = "https://files.pythonhosted.org/packages/e2/23/fce251cd6b0546dfc181b00d5c8ef1c95a8c4cae83266bc3dfd5f719c62c/pyarrow-26.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:290a74c48e9491b436fd5edacfadf357943f82aa45c81110bd83a69aab33d1cf", size = 57388010, upload-time = "2026-10-09T08:24:48.186Z" },
1056+ { url = "https://files.pythonhosted.org/packages/44/a5/0126fb0ef8d59bf257bdd68bb41623b72afc6e81790a0b4ac863a0f58861/pyarrow-26.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:515a10dae2a1d236bc9c9209d0317acb6746ea63cd4f98704904af7156d90ed1", size = 29406123, upload-time = "2026-10-09T08:24:53.387Z" },
1057+ { url = "https://files.pythonhosted.org/packages/ed/66/8ada1b5165359d84b4b9b5384742304d1081da670f77d458fd9c9b8a2161/pyarrow-26.0.0-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e890816e5ee89c74a0f8b9379fe8b5ba83f46132b2a0bbb9b1c21359ec30dfda", size = 36373215, upload-time = "2026-10-09T08:25:03.067Z" },
1058+ { url = "https://files.pythonhosted.org/packages/c4/83/74f10c3d803a6834b2acab21847724d4bdbc74d246eb17321432844707f3/pyarrow-26.0.0-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:9db18a9dc0af52135c9eac549d80a7a882696efbe5406cf882b044525d4ecc2e", size = 38730866, upload-time = "2026-10-09T08:25:07.924Z" },
1059+ { url = "https://files.pythonhosted.org/packages/e2/5a/ea2fa2163b1bd8ff73efd39c4060be63fd6ddec03e7887a471acd1e042a4/pyarrow-26.0.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:734312d3d99088d9ec28c5b17bad40389bd8373a1afc10acb60b83fd217af087", size = 50924443, upload-time = "2026-10-09T08:25:13.864Z" },
1060+ { url = "https://files.pythonhosted.org/packages/78/80/8c47b6cf8cfd42826df65193eff026c1cc81fa6cb213a3c3f5d203e6f67a/pyarrow-26.0.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:24f892fdf1ae1942d69d3f7742e2f49960ec95277cfb1a70b8a1d91f4a96d935", size = 53948540, upload-time = "2026-10-09T08:25:19.305Z" },
1061+ { url = "https://files.pythonhosted.org/packages/69/1f/3a506a76d944ec5c5e4b7f01d8d0446b392a6fb384de627a12e503f616b4/pyarrow-26.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:879331ddea2a26479fa18fade71e6facf684a6cf19f67daec3775c871569e8e5", size = 54494863, upload-time = "2026-10-09T08:25:24.517Z" },
1062+ { url = "https://files.pythonhosted.org/packages/3d/50/08c4bb04d651788d2eaca78065743f4f6ded974d4ef96ae3c473993e9d0c/pyarrow-26.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:5b827650e874f1f9f9392524ea3e9e3e8a245de5ba64acca1f81ab188090afb9", size = 57409877, upload-time = "2026-10-09T08:25:31.157Z" },
1063+ { url = "https://files.pythonhosted.org/packages/d4/f3/c64781fbd7b6d3c07993b698c14944d0d195f07e800fa931c486ae6ab36a/pyarrow-26.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:8e8e28c464552b5ca03e30d4504168c4425ce383884f8611b00e972f9fd933fc", size = 29236658, upload-time = "2026-10-09T08:26:22.607Z" },
1064+ { url = "https://files.pythonhosted.org/packages/06/55/2ee3729daea999f19f061f03898d4895a242c4cd94f26e1324e5fdfbfe10/pyarrow-26.0.0-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:ce28748cbeb0f29c3ce9603782979c7117580fc76f16aa3ca448b38a22281adb", size = 36489011, upload-time = "2026-10-09T08:25:37.64Z" },
1065+ { url = "https://files.pythonhosted.org/packages/6a/7d/3eb17f601f2bf13eda5f2ed28956379ca628b4dda97619cbb1cb1721622d/pyarrow-26.0.0-cp315-cp315t-macosx_12_0_x86_64.whl", hash = "sha256:106bb9290fc6fd9a84138a9440038ef184bac86463543c5ff099229cb30d996c", size = 38808480, upload-time = "2026-10-09T08:25:43.579Z" },
1066+ { url = "https://files.pythonhosted.org/packages/0e/e3/f0047360b0f4bfc031b256dc0aec3837a61f245b2fb70f8363438e2db665/pyarrow-26.0.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a413046eba9896e632925066c74095182200ba32e19ff0166bf64d2f936ac", size = 50923273, upload-time = "2026-10-09T08:25:51.445Z" },
1067+ { url = "https://files.pythonhosted.org/packages/38/d9/56d9fb91210407df31cbeb9b91138601c88c7c8fb5f6bf773b20d65509bf/pyarrow-26.0.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:d58798c4d8d629700058e9afc1e16b9801023f3ce4dc1c92d945e79b5ffe4e98", size = 53900905, upload-time = "2026-10-09T08:25:59.554Z" },
1068+ { url = "https://files.pythonhosted.org/packages/cf/40/8e8a7e9e027c731520c7eb179dd00a153b76ebf0bc11d213c6c8f8502851/pyarrow-26.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:645917e976671debabf854abab6e2b75c571ca4f82adc33a2d338697f7c27d93", size = 54518345, upload-time = "2026-10-09T08:26:07.125Z" },
1069+ { url = "https://files.pythonhosted.org/packages/be/89/1e768a3fdb88d34e708ad2dc00dbf8e4e30290784eb84198d59308963bea/pyarrow-26.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7c3fda041e7078802589cf257750323ee3d0cd1e56e53a9b20ec845697fb3d28", size = 57379403, upload-time = "2026-10-09T08:26:13.624Z" },
1070+ { url = "https://files.pythonhosted.org/packages/96/be/7b81a44d6a8e70581dcc1d6f01541f9000a973b1e5d75394aec91e7b179a/pyarrow-26.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:68cd662e9e2b00876a131950cf32336ace2d0865e1f9418763e3d3be8481dfa4", size = 29389953, upload-time = "2026-10-09T08:26:18.277Z" },
1071+]
1072+
1073+[[package]]
1074+name = "pydantic"
1075+version = "2.14.1"
1076+source = { registry = "https://pypi.org/simple" }
1077+dependencies = [
1078+ { name = "annotated-types" },
1079+ { name = "pydantic-core" },
1080+ { name = "typing-extensions" },
1081+ { name = "typing-inspection" },
1082+]
1083+sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" }
1084+wheels = [
1085+ { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" },
1086+]
1087+
1088+[[package]]
1089+name = "pydantic-core"
1090+version = "2.50.1"
1091+source = { registry = "https://pypi.org/simple" }
1092+dependencies = [
1093+ { name = "typing-extensions" },
1094+]
1095+sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" }
1096+wheels = [
1097+ { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" },
1098+ { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" },
1099+ { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" },
1100+ { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" },
1101+ { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" },
1102+ { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" },
1103+ { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" },
1104+ { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" },
1105+ { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" },
1106+ { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" },
1107+ { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" },
1108+ { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" },
1109+ { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" },
1110+ { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" },
1111+ { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" },
1112+ { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" },
1113+ { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" },
1114+ { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" },
1115+ { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" },
1116+ { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" },
1117+ { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" },
1118+ { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" },
1119+ { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" },
1120+ { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" },
1121+ { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" },
1122+ { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" },
1123+ { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" },
1124+ { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" },
1125+ { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" },
1126+ { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" },
1127+ { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" },
1128+ { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" },
1129+ { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" },
1130+ { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" },
1131+ { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" },
1132+ { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" },
1133+ { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" },
1134+ { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" },
1135+ { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" },
1136+ { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" },
1137+ { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" },
1138+ { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" },
1139+ { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" },
1140+ { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" },
1141+ { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" },
1142+ { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" },
1143+ { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" },
1144+ { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" },
1145+ { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" },
1146+ { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" },
1147+ { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" },
1148+ { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" },
1149+ { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" },
1150+ { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" },
1151+ { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" },
1152+ { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" },
1153+ { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" },
1154+ { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" },
1155+ { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" },
1156+ { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" },
1157+ { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" },
1158+ { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" },
1159+ { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" },
1160+ { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" },
1161+ { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" },
1162+ { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" },
1163+ { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" },
1164+ { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" },
1165+ { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" },
1166+ { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" },
1167+ { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" },
1168+ { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" },
1169+ { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" },
1170+ { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" },
1171+ { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" },
1172+ { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" },
1173+ { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" },
1174+ { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" },
1175+ { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" },
1176+ { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" },
1177+ { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" },
1178+ { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" },
1179+ { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" },
1180+ { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" },
1181+ { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" },
1182+ { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" },
1183+ { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" },
1184+ { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" },
1185+ { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" },
1186+ { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" },
1187+ { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" },
1188+ { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" },
1189+ { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" },
1190+ { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" },
1191+ { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" },
1192+]
1193+
1194+[[package]]
1195+name = "pygments"
1196+version = "2.21.0"
1197+source = { registry = "https://pypi.org/simple" }
1198+sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
1199+wheels = [
1200+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
1201+]
1202+
1203+[[package]]
1204+name = "pyparsing"
1205+version = "3.3.3"
1206+source = { registry = "https://pypi.org/simple" }
1207+sdist = { url = "https://files.pythonhosted.org/packages/e4/11/b213bebff182584360cb8d17c72c1677fec5c5c228de439e63bcf8ab1c8f/pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", size = 1050487, upload-time = "2026-09-20T20:59:05.609Z" }
1208+wheels = [
1209+ { url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" },
1210+]
1211+
1212+[[package]]
1213+name = "pytest"
1214+version = "9.2.0"
1215+source = { registry = "https://pypi.org/simple" }
1216+dependencies = [
1217+ { name = "colorama", marker = "sys_platform == 'win32'" },
1218+ { name = "iniconfig" },
1219+ { name = "packaging" },
1220+ { name = "pluggy" },
1221+ { name = "pygments" },
1222+]
1223+sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" }
1224+wheels = [
1225+ { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" },
1226+]
1227+
1228+[[package]]
1229+name = "pytest-xdist"
1230+version = "3.8.0"
1231+source = { registry = "https://pypi.org/simple" }
1232+dependencies = [
1233+ { name = "execnet" },
1234+ { name = "pytest" },
1235+]
1236+sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
1237+wheels = [
1238+ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
1239+]
1240+
1241+[[package]]
1242+name = "python-dateutil"
1243+version = "3.0.0"
1244+source = { registry = "https://pypi.org/simple" }
1245+dependencies = [
1246+ { name = "six" },
1247+ { name = "tzdata" },
1248+]
1249+sdist = { url = "https://files.pythonhosted.org/packages/0e/df/f15f80383f152452d8985b36d95cb8751f901a3e0ff4cd27f7e118f168e2/python_dateutil-3.0.0.tar.gz", hash = "sha256:7ebcb3c29a92f3ceafe79a7a48a5c45677e9e004bbeec5742ac0bbb00560b7a2", size = 271066, upload-time = "2026-10-21T23:41:45.075Z" }
1250+wheels = [
1251+ { url = "https://files.pythonhosted.org/packages/5a/17/cebee01d1c0c1441174cf0456ddc91b280d79acb65340c05e41d77866ee8/python_dateutil-3.0.0-py2.py3-none-any.whl", hash = "sha256:3c79630202e4152c6add678446984236efe4f57e84d8e892aab0637bab82e9fd", size = 94659, upload-time = "2026-10-21T23:41:43.431Z" },
1252+]
1253+
1254+[[package]]
1255+name = "python-dotenv"
1256+version = "1.2.4"
1257+source = { registry = "https://pypi.org/simple" }
1258+sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
1259+wheels = [
1260+ { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
1261+]
1262+
1263+[[package]]
1264+name = "pyyaml"
1265+version = "6.0.3"
1266+source = { registry = "https://pypi.org/simple" }
1267+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
1268+wheels = [
1269+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
1270+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
1271+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
1272+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
1273+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
1274+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
1275+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
1276+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
1277+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
1278+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
1279+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
1280+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
1281+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
1282+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
1283+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
1284+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
1285+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
1286+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
1287+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
1288+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
1289+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
1290+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
1291+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
1292+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
1293+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
1294+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
1295+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
1296+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
1297+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
1298+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
1299+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
1300+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
1301+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
1302+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
1303+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
1304+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
1305+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
1306+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
1307+]
1308+
1309+[[package]]
1310+name = "ruff"
1311+version = "0.17.3"
1312+source = { registry = "https://pypi.org/simple" }
1313+sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" }
1314+wheels = [
1315+ { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" },
1316+ { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" },
1317+ { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" },
1318+ { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" },
1319+ { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" },
1320+ { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" },
1321+ { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" },
1322+ { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" },
1323+ { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" },
1324+ { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" },
1325+ { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" },
1326+ { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" },
1327+ { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" },
1328+ { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" },
1329+ { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" },
1330+ { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" },
1331+ { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" },
1332+]
1333+
1334+[[package]]
1335+name = "scipy"
1336+version = "1.18.1"
1337+source = { registry = "https://pypi.org/simple" }
1338+dependencies = [
1339+ { name = "numpy" },
1340+]
1341+sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" }
1342+wheels = [
1343+ { url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" },
1344+ { url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" },
1345+ { url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" },
1346+ { url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" },
1347+ { url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" },
1348+ { url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" },
1349+ { url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" },
1350+ { url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" },
1351+ { url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" },
1352+ { url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" },
1353+ { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" },
1354+ { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" },
1355+ { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" },
1356+ { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" },
1357+ { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" },
1358+ { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" },
1359+ { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" },
1360+ { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" },
1361+ { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" },
1362+ { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" },
1363+ { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" },
1364+ { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" },
1365+ { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" },
1366+ { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" },
1367+ { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" },
1368+ { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" },
1369+ { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" },
1370+ { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" },
1371+ { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" },
1372+ { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" },
1373+ { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" },
1374+ { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" },
1375+ { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" },
1376+ { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" },
1377+ { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" },
1378+ { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" },
1379+ { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" },
1380+ { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" },
1381+ { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" },
1382+ { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" },
1383+ { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" },
1384+ { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" },
1385+ { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" },
1386+ { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" },
1387+ { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" },
1388+ { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" },
1389+ { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" },
1390+ { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" },
1391+ { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" },
1392+ { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" },
1393+ { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" },
1394+ { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" },
1395+ { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" },
1396+ { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" },
1397+ { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" },
1398+ { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" },
1399+ { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" },
1400+ { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" },
1401+ { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" },
1402+ { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" },
1403+]
1404+
1405+[[package]]
1406+name = "six"
1407+version = "1.17.0"
1408+source = { registry = "https://pypi.org/simple" }
1409+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
1410+wheels = [
1411+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
1412+]
1413+
1414+[[package]]
1415+name = "sniffio"
1416+version = "1.3.1"
1417+source = { registry = "https://pypi.org/simple" }
1418+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
1419+wheels = [
1420+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
1421+]
1422+
1423+[[package]]
1424+name = "sortedcontainers"
1425+version = "2.4.0"
1426+source = { registry = "https://pypi.org/simple" }
1427+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
1428+wheels = [
1429+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
1430+]
1431+
1432+[[package]]
1433+name = "statsmodels"
1434+version = "0.15.0"
1435+source = { registry = "https://pypi.org/simple" }
1436+dependencies = [
1437+ { name = "formulaic" },
1438+ { name = "numpy" },
1439+ { name = "packaging" },
1440+ { name = "pandas" },
1441+ { name = "patsy" },
1442+ { name = "scipy" },
1443+]
1444+sdist = { url = "https://files.pythonhosted.org/packages/a8/1f/a3cbf6ed7cf6286afec694bfef561f3e51483197716717fb8476f1f0c50e/statsmodels-0.15.0.tar.gz", hash = "sha256:5d257fe58d0772bc46a557880ca78e2a8e07fec7bfd9d11074aef8e33e1aecbc", size = 15736853, upload-time = "2026-08-27T10:45:21.988Z" }
1445+wheels = [
1446+ { url = "https://files.pythonhosted.org/packages/1f/17/14121b56377f2881ce94c0d0a01466585468923340d94841337d324f6be6/statsmodels-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e8deb1e3d4ab89fec2a52815f1ec924920c4e0cdb307421a4e47cfed87c03ff", size = 11654329, upload-time = "2026-08-27T15:09:42.207Z" },
1447+ { url = "https://files.pythonhosted.org/packages/ef/f5/6b550b5c936dd9ec5c859477698b4fcbccc826e9a4a4a52847fa51673357/statsmodels-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6e20788141df134b83ffb43b8cccf135a876188f84124835c86f011f138adee", size = 11527231, upload-time = "2026-08-27T10:35:42.669Z" },
1448+ { url = "https://files.pythonhosted.org/packages/f0/a5/0e7e87e0dedc68bfeecc24bea92876023db09bac1c0fe50584a7d0819fb5/statsmodels-0.15.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f90dc4989bd82837c80059fdce132976d4e5a80100119d16e1b00537eb55867", size = 11747428, upload-time = "2026-08-27T14:32:28.987Z" },
1449+ { url = "https://files.pythonhosted.org/packages/41/c7/aeadca282baf51d7b9c135674bac79ded91f4db79c69c188575d22234342/statsmodels-0.15.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f2bd48915cc9b3b95c3c7f6ac363b6f8aaa38172962ba5f7f6885f0dfe57f2", size = 12042643, upload-time = "2026-08-27T14:33:06.022Z" },
1450+ { url = "https://files.pythonhosted.org/packages/9c/44/7d3eb3836b3c054280baca7ec91bd12858825de437c6319c1212c2531d94/statsmodels-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:733887a09ff878a2e29504eda684488b26bd134ba3539ac58d2136a58914c65a", size = 12079497, upload-time = "2026-08-27T14:33:47.147Z" },
1451+ { url = "https://files.pythonhosted.org/packages/03/e3/d863228849da191d37c54bdd72634fbbcfadd12e910e29e4bdca1b952bb6/statsmodels-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba89eb3ccb1c9c1e400d5081157305495b09350b3a65f2d66d3424ad1d3c4209", size = 11322106, upload-time = "2026-08-27T10:35:58.135Z" },
1452+ { url = "https://files.pythonhosted.org/packages/87/17/fee8cfc100c37b1c553515dbad59607a55085a5b9beac2b008e4c03b6388/statsmodels-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09fe34ac80e147cd76cb702ba672559ee3c5dd3ba15c25a232922b46e5ffe70e", size = 11696848, upload-time = "2026-08-27T10:36:13.782Z" },
1453+ { url = "https://files.pythonhosted.org/packages/9e/60/a885751cd3aa796ef94b60f6d3de0c9ace4190feb3e55414ee728d2a0abc/statsmodels-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ffe08752f420f12a0b03e8f7d8f3a45417615309b1f37f10561f9a53f044737", size = 11556170, upload-time = "2026-08-27T10:36:29.848Z" },
1454+ { url = "https://files.pythonhosted.org/packages/0f/f4/482701d2dc7540897b13ead827cf8499b11c7325753b8cc4ff1a82c87a18/statsmodels-0.15.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a7ab98634d47048c6b7c168b3f8f71369e256d7314bc386168526f34c7699d7", size = 11752604, upload-time = "2026-08-27T10:47:50.809Z" },
1455+ { url = "https://files.pythonhosted.org/packages/cc/88/5c8819f232448d52514b76a64cf285bc608cff9179f3eeacf58750612323/statsmodels-0.15.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcaa1e4499697c4e023e5fa6fb04459a34e4f4a449c45ae441992e933bde24db", size = 12055386, upload-time = "2026-08-27T10:49:13.7Z" },
1456+ { url = "https://files.pythonhosted.org/packages/19/a8/298e9b5c993cad3b58913d61b7e7cb0e2fba12e39d3114cd8484f922a5d1/statsmodels-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:88dbe345ecf317862481b3c86305f42133082e5b366a3a8cf240a6caac404daf", size = 12088356, upload-time = "2026-08-27T10:48:10.645Z" },
1457+ { url = "https://files.pythonhosted.org/packages/28/f5/386c6cfda27c47df81643dc9466b3e28dc2a82152e524131977275a88e0c/statsmodels-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:ac9bce461d0c8f529f8ed331a469eaa5dd7c3213c06b62623a2e5438a6383093", size = 9809418, upload-time = "2026-08-27T10:36:50.646Z" },
1458+ { url = "https://files.pythonhosted.org/packages/15/b4/a8a4d89918599309bebfc09a2f793a910279f3900671bc959ac8c810edd1/statsmodels-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:081adf7e5f2da63f63cbd90491447de5d4c4c2921f19814f235bcf3e736edd3c", size = 11312877, upload-time = "2026-08-27T10:37:08.325Z" },
1459+ { url = "https://files.pythonhosted.org/packages/7a/79/66b57afdcbf9a8b21b1a8e4414ddb006e819b2a81243cd74c12f3301eca3/statsmodels-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3e5f870037ca154a4d177aeac68704a784b24cf2c45d86b046fc28e37a2a5584", size = 10812282, upload-time = "2026-08-27T10:37:26.708Z" },
1460+ { url = "https://files.pythonhosted.org/packages/86/51/daf737efb6002905bd006d296f9ad031ed3861b640075bde3b734706b49d/statsmodels-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:04e65ee05475aeceb9cb954cf96c0f9361b9cdfd497b8aae47afbab0c7c786dd", size = 11698848, upload-time = "2026-08-27T10:37:46.312Z" },
1461+ { url = "https://files.pythonhosted.org/packages/22/b1/ca42c91e427db43917d4ab5cc6991e7e50b574bcfdf6c4c8eb9590af71d3/statsmodels-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1f1443cc1ff7683f3d7a9054e6dccd92040fbd4324616a0b6dbb8af1ce7889fb", size = 11571229, upload-time = "2026-08-27T10:38:04.723Z" },
1462+ { url = "https://files.pythonhosted.org/packages/5b/04/d803e8d014d4643d9c0a5eec78343073fbfff1c1018f776dd416c07fc076/statsmodels-0.15.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a45026596c71afa18543ec2af40b895609c181fc3649433c1cded5ce23ec5cd4", size = 11801588, upload-time = "2026-08-27T15:00:31.603Z" },
1463+ { url = "https://files.pythonhosted.org/packages/fd/44/40d7678858065e9ec8efd9f37f04227b22a66632ba194e4aa43fda023f6a/statsmodels-0.15.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:edd803f4606061af8c7a83e2326ea294cbfa802a908dfbda4a88347301fb6820", size = 12062254, upload-time = "2026-08-27T15:00:49.803Z" },
1464+ { url = "https://files.pythonhosted.org/packages/51/69/16c73c3c09f4b8d85f125d7d3ae0f7092d4736a83d24c0159e4fb21632ec/statsmodels-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:eb7fc1147cd8d17132190db7a8e63cd76a06145dec502816d8f3c20619923f2b", size = 12101259, upload-time = "2026-08-27T15:01:08.755Z" },
1465+ { url = "https://files.pythonhosted.org/packages/23/4c/1138aad8e3c27ebf941d732788488cce7c28d76404a7b7fc1186faad2890/statsmodels-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:c3c1138b4d0e5b0c2387b17dafd2a0fd137c67bd869f70d4ee56feecc918cb0d", size = 11348219, upload-time = "2026-08-27T15:11:12.924Z" },
1466+ { url = "https://files.pythonhosted.org/packages/3a/bd/53546d8c9ab013d9a0513706d2a09a03aff12034e6f898987e8ce0f38257/statsmodels-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:74c0b63448f03020cc27a8ee698fbcf2ba91e61a9f9eee7978c0c4ed33a9b17f", size = 10866645, upload-time = "2026-08-27T10:38:20.886Z" },
1467+ { url = "https://files.pythonhosted.org/packages/38/a1/0d38328643d4704c8de7867518c3f52ec145ff62537b2ca1e14cdeecf51b/statsmodels-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6197b4cca69fd7bc23ab9430abf1c479764cfe74767467f51964a633dfade7a5", size = 11870287, upload-time = "2026-08-27T10:38:38.271Z" },
1468+ { url = "https://files.pythonhosted.org/packages/28/55/fac7e03895f24a0b4883ff9fc69f9421a5df1b60e7edd7e68d49f4327593/statsmodels-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5538a734dd28efd450bb38a5d5fc94d13ee709bc497e3e4c0a24b3803e3eac39", size = 11755467, upload-time = "2026-08-27T10:38:54.683Z" },
1469+ { url = "https://files.pythonhosted.org/packages/4c/38/5321b9f6065d1c5861e87853720603d56ae81795a83d3040410ca2684305/statsmodels-0.15.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dde0657d18e322eedb2540a84c0582e97afd0f78c370b45eebb0d4ac228a7a83", size = 11636274, upload-time = "2026-08-27T15:03:41.86Z" },
1470+ { url = "https://files.pythonhosted.org/packages/28/56/c75e984c492509d9ab2ba5f2c0e4eab7177cd2cec424cef9dafb42f0cc70/statsmodels-0.15.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90e75e726be2c9fc1d1d4c43337503a35864bdf5b4693773a08c6ee92d04a153", size = 11865818, upload-time = "2026-08-27T15:04:01.503Z" },
1471+ { url = "https://files.pythonhosted.org/packages/61/72/9e03975701c808767c19f394f670941a962204a4f6a00eeaf56b560348e8/statsmodels-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65e64afc2655a486ea44a5ffe4e2b05ee3abbcb10ea61d1c02d3e9e519338d9b", size = 11907800, upload-time = "2026-08-27T15:05:49.786Z" },
1472+ { url = "https://files.pythonhosted.org/packages/4f/a7/c467019a8bf1ab6a07ca88004f0a1d4ea2dae89642aa804e49437b256243/statsmodels-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:edee066ac9b171d95c3de925225331a21de1deb5b6452f252bc853e44566e72b", size = 11559083, upload-time = "2026-08-27T15:06:05.872Z" },
1473+ { url = "https://files.pythonhosted.org/packages/54/10/dc78352367cd5bb298340b57c390c944b66236f6d026cc238bc8264c7633/statsmodels-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:40b2737456e75d96866943e33017a1dffc9166025ee33b7eb373fbe8c7a85e09", size = 11029613, upload-time = "2026-08-27T10:39:10.043Z" },
1474+ { url = "https://files.pythonhosted.org/packages/de/cb/a1f5ad730cb66c7d911ebb45413880333d837f1cffff990a4c6d2c541b95/statsmodels-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:d48ba61db0d13d3033330e6bd1f864dfa9b48c0b952b3b0f7570206b35d552e5", size = 11702125, upload-time = "2026-08-30T16:04:07.635Z" },
1475+ { url = "https://files.pythonhosted.org/packages/ce/db/fb95f242adce112c1507e2062ad6b7b00aea338925f2a03fbe47973c4d72/statsmodels-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:24b9917a09bbe0227047e512a40652c4b9117c2cfbf19c4a5b9971a8944a8606", size = 11571622, upload-time = "2026-08-30T16:04:29.558Z" },
1476+ { url = "https://files.pythonhosted.org/packages/b5/3f/f4cc7e5f4d6c8f46551aee63e59d9b964881070f552400ebb76667b39ad9/statsmodels-0.15.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333d0c70f269ff98b647f157d2ae7fbcf0617e6f9a5b1817fc6609766502292c", size = 11780713, upload-time = "2026-08-30T07:40:06.332Z" },
1477+ { url = "https://files.pythonhosted.org/packages/e9/20/fa315a013f70efa6674adfaca59365ea7562371914c408551fc3c399792f/statsmodels-0.15.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b946d7c922f46f045f25e50daf13439f79c05e403ba50081cad509892c8b26", size = 12070117, upload-time = "2026-08-30T07:40:27.04Z" },
1478+ { url = "https://files.pythonhosted.org/packages/91/43/a63f7880050c2c24056fa8d663b7c8a1816b25864660abbc73afff3f3558/statsmodels-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cf897abce5616e2763ab7d7a7fa914677df87a649971f877b83a358c98f6e291", size = 12105153, upload-time = "2026-08-30T07:40:45.814Z" },
1479+ { url = "https://files.pythonhosted.org/packages/d4/d4/523ddb13862c228407297754486b6c9e8777e5114f4532b667b47728c544/statsmodels-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:511a4f9cadbf91a690bfa863bb3e25dce583d8140046886fdf35fbfabfbce401", size = 11352844, upload-time = "2026-08-30T16:04:48.14Z" },
1480+ { url = "https://files.pythonhosted.org/packages/53/59/f3058214e61b15582ef17d761c9333def46960ee521313409b2705791029/statsmodels-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:0cb6228713fe47046b606daee63ed4473a7c246fa97cf0a6d6f20d3b0c96c051", size = 10866685, upload-time = "2026-08-30T16:05:03.971Z" },
1481+ { url = "https://files.pythonhosted.org/packages/4a/2d/43089b37f4bbbf7373929cd198de6cbdb0b5a4580a6e5e32bf9fe74fccab/statsmodels-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:27e9467fae83ff327c9d16ae5ea7e771bcdd66a682f49aa324eeefea5584f2d5", size = 11858149, upload-time = "2026-08-30T16:05:20.585Z" },
1482+ { url = "https://files.pythonhosted.org/packages/7d/38/ca88421b81666cdcfc3d0eb12c714f80f2be327669ececbb5930b68a71c0/statsmodels-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ecf84918dcd410ab34d3d85d34e0b16ebb10319c9fc77dbf9a1f1a49324c59f8", size = 11739931, upload-time = "2026-08-30T16:05:37.286Z" },
1483+ { url = "https://files.pythonhosted.org/packages/ab/64/98b9e2b1fbfdc4e2392bff349718d8dc796403006a3106ec375c59a5d3f8/statsmodels-0.15.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5891ad077b36d6401e44967739df3d2cdd380a0e6e9e46dadec7e3d3a828af55", size = 11657996, upload-time = "2026-08-30T07:41:05.608Z" },
1484+ { url = "https://files.pythonhosted.org/packages/34/6d/d8c9e73013ee854031218e8a044ba679535268bc61cf59466f19ac8d980c/statsmodels-0.15.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af80f9658c48792947fc754a026ffbebca61a8b483253781973f604a67cf1f8c", size = 11893402, upload-time = "2026-08-30T07:41:27.553Z" },
1485+ { url = "https://files.pythonhosted.org/packages/cc/f5/b9b47f3c688a8af1b199af96e098b8f20897a60b45187c57ce6fcde77a5f/statsmodels-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:583eb2a46f7c7a5c34a26715c8b6d92199bfb4f04623db5fff9902a3c7c832d3", size = 11934786, upload-time = "2026-08-30T07:41:46.809Z" },
1486+ { url = "https://files.pythonhosted.org/packages/c4/08/608871f18bc18c6cba0b484e81fd25a023ef3ca6541f6793ec839fa49e1f/statsmodels-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:38038823fbe86f11e09433b4a0f4af5f102327c3137a1604b002ce3cf722a31f", size = 11549124, upload-time = "2026-08-30T16:05:53.179Z" },
1487+ { url = "https://files.pythonhosted.org/packages/e3/eb/90a506dbee5cde669e2ef28565ff6b7a0791ab415eab2da82f92d033cccc/statsmodels-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:90df413d7e09474f7e7cbf8807440c1bf76298cdd9daabf9141f76d7627c9151", size = 11021454, upload-time = "2026-08-30T16:06:08.124Z" },
1488+]
1489+
1490+[[package]]
1491+name = "tqdm"
1492+version = "4.70.1"
1493+source = { registry = "https://pypi.org/simple" }
1494+dependencies = [
1495+ { name = "colorama", marker = "sys_platform == 'win32'" },
1496+]
1497+sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" }
1498+wheels = [
1499+ { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" },
1500+]
1501+
1502+[[package]]
1503+name = "truststore"
1504+version = "0.10.5"
1505+source = { registry = "https://pypi.org/simple" }
1506+sdist = { url = "https://files.pythonhosted.org/packages/ee/9f/c5201d42a484c061e528825fc8e2d565f5abd50a4ced6fb7d29c4ec99b2b/truststore-0.10.5.tar.gz", hash = "sha256:30d36967ccaded5cbb38d602c433f53600036c79d502f4533a49b60a03bbefcd", size = 28091, upload-time = "2026-10-12T22:27:31.808Z" }
1507+wheels = [
1508+ { url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c", size = 19017, upload-time = "2026-10-12T22:27:30.377Z" },
1509+]
1510+
1511+[[package]]
1512+name = "typing-extensions"
1513+version = "4.16.0"
1514+source = { registry = "https://pypi.org/simple" }
1515+sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
1516+wheels = [
1517+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
1518+]
1519+
1520+[[package]]
1521+name = "typing-inspection"
1522+version = "0.4.4"
1523+source = { registry = "https://pypi.org/simple" }
1524+dependencies = [
1525+ { name = "typing-extensions" },
1526+]
1527+sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
1528+wheels = [
1529+ { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
1530+]
1531+
1532+[[package]]
1533+name = "tzdata"
1534+version = "2026.5"
1535+source = { registry = "https://pypi.org/simple" }
1536+sdist = { url = "https://files.pythonhosted.org/packages/d9/68/f1b440335057bfce71b6e50a9d09445aa2ecbd08359a337976627b8409e7/tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7", size = 200404, upload-time = "2026-10-03T09:23:14.143Z" }
1537+wheels = [
1538+ { url = "https://files.pythonhosted.org/packages/94/21/1e5995a1c920cce14e4bffae20c665ec10e7ed03ab25e006cd741092b718/tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac", size = 347996, upload-time = "2026-10-03T09:23:12.535Z" },
1539+]
1540+
1541+[[package]]
1542+name = "wrapt"
1543+version = "2.5.1"
1544+source = { registry = "https://pypi.org/simple" }
1545+sdist = { url = "https://files.pythonhosted.org/packages/04/22/89e2f3bdae5cb34e0cab0cd86d7172dbf418de4b46c9b17b9c7a560dfa44/wrapt-2.5.1.tar.gz", hash = "sha256:f595bb0185aab3e9dc31950c95d914f56ea8278810c3b928f3426e12ed6d27bc", size = 184455, upload-time = "2026-10-14T00:39:39.24Z" }
1546+wheels = [
1547+ { url = "https://files.pythonhosted.org/packages/66/9b/c7f97d5493a33b5ed01d3c85745f9bdfdd2e5c2785471b8d8b55a3c273d6/wrapt-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6e3eff05ae616671b40d7ad0a504210329e4adc9fb91415663570aca93c5f5cc", size = 106452, upload-time = "2026-10-14T00:37:18.951Z" },
1548+ { url = "https://files.pythonhosted.org/packages/7f/b0/335b0af2930938678fcde954b29780b26308961b93df5e0192fc182e8b7e/wrapt-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c44dd9881626da7d621c23805f26726f6b023cf3e9755f48d092bc9cbef4a8e7", size = 106087, upload-time = "2026-10-14T00:37:20.392Z" },
1549+ { url = "https://files.pythonhosted.org/packages/4a/5a/2a34ba5a468e9d3d6e5b0733280e1ae3c850bfc5f1d681fc0e97f564d1f2/wrapt-2.5.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bfaa998ceeea4d0aa72b40cdd0023d19409504e244b439ff2aa9f01729341c5f", size = 250438, upload-time = "2026-10-14T00:37:21.882Z" },
1550+ { url = "https://files.pythonhosted.org/packages/b3/d5/3d4ad322af74d3ab2a14f69ba844cdd3edefb555edfc1c1976ec0112d5c4/wrapt-2.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6d274ec50a5b208be75596dc44ea253e65deaa6ee3a600babc86dafbb957dfc", size = 250594, upload-time = "2026-10-14T00:37:23.497Z" },
1551+ { url = "https://files.pythonhosted.org/packages/37/1a/3cbf48425ec2c66aa9645218458da1e19e315abb9766604d3c49e579076c/wrapt-2.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1a96e2671c60f9f09ae547b5a815cecb29af16caa68d73693387d0028788cb32", size = 229856, upload-time = "2026-10-14T00:37:25.029Z" },
1552+ { url = "https://files.pythonhosted.org/packages/cc/e7/b2ea57f4c51258659200565af8617d76992b0fe65e6aad7162dd5720ef05/wrapt-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:729d644b6acaf4846a4ef81b037857b66a01dea6d227f827c6d71c0b6d656d6c", size = 247153, upload-time = "2026-10-14T00:37:26.67Z" },
1553+ { url = "https://files.pythonhosted.org/packages/9d/c1/4714743e672ed1084a035a2a4f0edeef7838399753b4856a0dc46ef9487d/wrapt-2.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:859f67bfc31eb7ab55f237b629cd4ab0441b075912446481f910f7d02066811e", size = 226726, upload-time = "2026-10-14T00:37:28.425Z" },
1554+ { url = "https://files.pythonhosted.org/packages/91/e3/c00401bcc3485eb9937c3fe4a1cc8fc3b61800b1378ea3a143ea1c30f6f6/wrapt-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:29b62e87fcd6a1893f669abfd02a596a7fc5cfa79fa57e42c4e650a6c170c67b", size = 238843, upload-time = "2026-10-14T00:37:30.075Z" },
1555+ { url = "https://files.pythonhosted.org/packages/76/b5/c16759fb0721e63df92b576c2222ce1f11690a8b300fd91b49c55436865c/wrapt-2.5.1-cp312-cp312-win32.whl", hash = "sha256:f1c911818fb076910ef509f2298dfcb966a54a6ff068eebd459632102cf589fb", size = 100488, upload-time = "2026-10-14T00:37:31.625Z" },
1556+ { url = "https://files.pythonhosted.org/packages/22/d5/39d5a704650f18799f37841442b464edb81cf2015f006eaef26068acc6ea/wrapt-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:c39c7130ea0702c4ab0faf12da1df1e02d5174305c17edf02309e2f058c4114f", size = 106048, upload-time = "2026-10-14T00:37:33.188Z" },
1557+ { url = "https://files.pythonhosted.org/packages/21/bf/65743adeeb5476920c62dad6cded7bc8789e19bd4f9a336d4ac812adb8de/wrapt-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:e089a22ff5af1290b8c759a610830bdb2a829ef9c3d7797e4ee32c2f795ed482", size = 103200, upload-time = "2026-10-14T00:37:34.673Z" },
1558+ { url = "https://files.pythonhosted.org/packages/e4/6d/cfe55762435f36107815d56a2cfbebe7e3129b593c47a670c6eb1d7917d3/wrapt-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f98eaf784cd12bc69c77af398084174531007cd81849c962163ccfc6e791f3ea", size = 106068, upload-time = "2026-10-14T00:37:36.087Z" },
1559+ { url = "https://files.pythonhosted.org/packages/01/b9/41642877fe741db56d240833c8822188b663c4c5d52beb087964774035d4/wrapt-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab6db7d2a18d366cc57c2228253cf26443190aba0a6dd0939b3c1e8ac6e29e2c", size = 106190, upload-time = "2026-10-14T00:37:37.768Z" },
1560+ { url = "https://files.pythonhosted.org/packages/37/62/20edad100b93552ec5c172e509a9db898a73d5043ae701fcb6e9986f9d33/wrapt-2.5.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1630201b0e2a96bb26304b7adfbd91a4ef486abb5a4c48377444a0bed749f37", size = 248519, upload-time = "2026-10-14T00:37:39.321Z" },
1561+ { url = "https://files.pythonhosted.org/packages/3d/e9/8d81185bc9a40cfb43d91fc70a1e80ecde752c95dc98f5452cae82037976/wrapt-2.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800c7689154622b0ba2922ceca44a3cf2ef61c3b9a4c4eeb1d8b3050d7ededa", size = 248488, upload-time = "2026-10-14T00:37:40.96Z" },
1562+ { url = "https://files.pythonhosted.org/packages/8a/88/8431df4fd81f0dfa83e8ede463eed311d083c5a279a56891dc0396b07b0e/wrapt-2.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b53000b424dc2133eaaf22838a2352d3497f5d7c2e7d9a2acfe675ab7225bb1", size = 227392, upload-time = "2026-10-14T00:37:42.599Z" },
1563+ { url = "https://files.pythonhosted.org/packages/db/8a/ee6f8542eeccad6874faf0b7b2e129952c527a482f1d28940e2111fec2d6/wrapt-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76f230a9b07e3cb66646d265398f579abb6128b1bb4cb97c74b1ae5d09e96f31", size = 244972, upload-time = "2026-10-14T00:37:44.209Z" },
1564+ { url = "https://files.pythonhosted.org/packages/4d/1f/32c59e7fd522409f3863dfecdab5315ee9ba37f96020b6f0adee9d223310/wrapt-2.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd3f878a4aac3c262447ddf43c5f4c18fc67dfc3ba69c4fb1c7a4c4af96abe7e", size = 225316, upload-time = "2026-10-14T00:37:45.948Z" },
1565+ { url = "https://files.pythonhosted.org/packages/ae/d6/1b9abc1244592034c5db744571e17d663f0f1b0ce6c8ba279c60f6f9c3a8/wrapt-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c9480bdee340a1602cae5a777146ab4be3e384fdcb569fffdf8721032314645", size = 237145, upload-time = "2026-10-14T00:37:47.535Z" },
1566+ { url = "https://files.pythonhosted.org/packages/f0/ce/8f3b5482f768c1d60fd2557d049c766543fef5ec707037cb410a57eb65ee/wrapt-2.5.1-cp313-cp313-win32.whl", hash = "sha256:dc401274fcc7b15b3b2c12df2ff34024a11925243a7d3daee91c6d7d14f9addf", size = 100462, upload-time = "2026-10-14T00:37:49.21Z" },
1567+ { url = "https://files.pythonhosted.org/packages/7b/dc/6a5735874ea79816f85c1ec9d92139d7073c20d1881c15ff2108c211354b/wrapt-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:09b1893ee4063706574c1813abf479b8b51926633fbdb6f96aab8dc7b0976668", size = 105870, upload-time = "2026-10-14T00:37:50.745Z" },
1568+ { url = "https://files.pythonhosted.org/packages/08/83/a4e8b5a5a32f8dfc5dad8344f1e2b908f7d8d84b11c3c336bf7f79a5144a/wrapt-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f280c115ea64eff3dcbd68a668ce3f63476a4ba386bbabb318017e286196ea2c", size = 102903, upload-time = "2026-10-14T00:37:52.323Z" },
1569+ { url = "https://files.pythonhosted.org/packages/25/3d/ec1937283863bbe0d90528e09f2b27cfc0dd7e608fc2b65c804967dee369/wrapt-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cf63fffcdcd8c60f223d3967bb92cc4fc2e8b46f09e75b67a6a75e6f47c0fc43", size = 106439, upload-time = "2026-10-14T00:37:53.853Z" },
1570+ { url = "https://files.pythonhosted.org/packages/93/39/cca8afb80dbb9fce6103e59db507a9415291c4dbe97ea055875ff62901fb/wrapt-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f0750cbc2e29e4f3c9529d3587d4e7ed8f60638ceafb80b87a95833b0c5acd9", size = 106378, upload-time = "2026-10-14T00:37:55.386Z" },
1571+ { url = "https://files.pythonhosted.org/packages/aa/a0/e784d7a9fd277a2ee395490ec4df96608b7fe218bb1ef7dced2a1caea490/wrapt-2.5.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3cf273b7e8d2038abb7f0a8c6550aff4f617b9d486a9965c8e8acc96a3a04de9", size = 251505, upload-time = "2026-10-14T00:37:57.022Z" },
1572+ { url = "https://files.pythonhosted.org/packages/81/56/01ebc86b88056f5782b9d50f962fb398c6b82aa11efc98f50c64896d94e3/wrapt-2.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380f72610181883f66b41442cfc7c0f7552b42169efb2113def26e6380013d37", size = 250744, upload-time = "2026-10-14T00:37:58.652Z" },
1573+ { url = "https://files.pythonhosted.org/packages/5f/5c/0e8eaaf31e2d6e7bf13c6eae2fd5b85eaa24e21e06466e6e7a0f35532689/wrapt-2.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef2a8f006410b6134a0d273ec037fea8cc7a6a914f1bd7555ad9788ad788c6e", size = 231598, upload-time = "2026-10-14T00:38:00.505Z" },
1574+ { url = "https://files.pythonhosted.org/packages/6a/6b/6a3e257e65de6cc0027e78b423942697c74451520ba3797fd86455ec4df8/wrapt-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9bad4dbb4e61624fcce5f301e37f9e743ecae4f1259a3777b3207eb7eba3dccd", size = 247998, upload-time = "2026-10-14T00:38:02.63Z" },
1575+ { url = "https://files.pythonhosted.org/packages/22/38/b2b8f3ee22b05f33f5aefb052844a36a0d7edd1eaff2ff4249f97792bea8/wrapt-2.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9a34640eb6295f33ca23462977de275fe8f3a50ab339b8918b96d69a7451e2e1", size = 228795, upload-time = "2026-10-14T00:38:04.317Z" },
1576+ { url = "https://files.pythonhosted.org/packages/29/39/e6c86552286ac27b855042fb9c229c580ed2d4c3ced5d0a1dad5f5ee8c11/wrapt-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26313f38d18d40a9975123a4ebff9da125ec63ab9ece4f05320a3d8d37d2c1fe", size = 239090, upload-time = "2026-10-14T00:38:06.119Z" },
1577+ { url = "https://files.pythonhosted.org/packages/e7/7a/aea209f64e894573935b17de26ecfa0efe139b63f170a60be9f13734e0f5/wrapt-2.5.1-cp314-cp314-win32.whl", hash = "sha256:0591e6eace0d186c9ef1ecd1244be5a04e98041424cfca425b684ffe4f0d8030", size = 100919, upload-time = "2026-10-14T00:38:07.874Z" },
1578+ { url = "https://files.pythonhosted.org/packages/57/24/847096aa49d42990137ed3b940743c8a6da806d39f6c455317114e8ebfde/wrapt-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:25ed8b1b39234140d5b5c6a273130c7595e0abece417c3ca3cb378fcea5cd0fe", size = 106243, upload-time = "2026-10-14T00:38:09.49Z" },
1579+ { url = "https://files.pythonhosted.org/packages/9c/ff/1cdc742133b9fb8558cdf42b2a6c2699bd7c72f7d0606286ec2f9142e20a/wrapt-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:6201c7e122f40060a9b50696d80deec8f93b1a235ec0443f51d7a8a42f7044a6", size = 103974, upload-time = "2026-10-14T00:38:11.354Z" },
1580+ { url = "https://files.pythonhosted.org/packages/fc/6f/c32dc64900f1970a7f991ff5d06788cd636ca2f3ee2f99709d82577ca198/wrapt-2.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:da847332447db5505162759a4cd5ac374eb8b74841fe97a98ef3de14edd2586d", size = 109552, upload-time = "2026-10-14T00:38:12.965Z" },
1581+ { url = "https://files.pythonhosted.org/packages/e9/73/a9c8cc82b166e3de42f5fbd88089d2ef9b72e87aac7d6cdddb070335c20b/wrapt-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f437dd704abc4ee1bd03bb2d796d362d0e75915e8f3113a7900b3b7ec5f8b47", size = 109853, upload-time = "2026-10-14T00:38:14.565Z" },
1582+ { url = "https://files.pythonhosted.org/packages/f6/48/f341d82e69ae47df2755847af1c744ff0732543482dbc2373e5e57676621/wrapt-2.5.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03aa7d2256309b57ddbf317bff2cae5f47e50ea9ae8d582780ebe0b554347b42", size = 284103, upload-time = "2026-10-14T00:38:16.23Z" },
1583+ { url = "https://files.pythonhosted.org/packages/fd/50/b87c6374377b08ee0783b6c5c31cd41a5e103bc54e7e857e800a0a965550/wrapt-2.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fcccaa1484f7dd1091602970988ab741491f9f974013c844f70e45ac1196b80d", size = 293883, upload-time = "2026-10-14T00:38:17.986Z" },
1584+ { url = "https://files.pythonhosted.org/packages/38/e0/6d0810ae73f7a5180ec366577588ab3ad55fc1bc7e3623e82d5f8528dd2a/wrapt-2.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8078186f719a92693199f1e06c4ec72e1e6d374c2e459da18ed5c39d6966d727", size = 263097, upload-time = "2026-10-14T00:38:20.107Z" },
1585+ { url = "https://files.pythonhosted.org/packages/d6/d3/c890a46f4d395a7935e5a7436f374ceefa362eb612fdd39376dd775e0283/wrapt-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425fcf0e70b27053bd610d57bae975856e7897e3f6ba1456d2b80b9d7fd15d1", size = 289045, upload-time = "2026-10-14T00:38:21.913Z" },
1586+ { url = "https://files.pythonhosted.org/packages/e5/c6/042e30e0d851ca6ea743e6978902527f9166da42d736f074245d1cc54c8f/wrapt-2.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b238e955ba34ef2b8897f358b7b868b41b9a02ffd338014b62985fa91898cc4a", size = 260378, upload-time = "2026-10-14T00:38:23.662Z" },
1587+ { url = "https://files.pythonhosted.org/packages/31/a4/5e65f90bf414c2f1c7eefc3d26c33af01d87ba33c4b879db4e1d4ba7fc3b/wrapt-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25eb4d928a9abeaf70ca786a35861b46d1ab37cc4ce49ea70a070dacdead4dfe", size = 278746, upload-time = "2026-10-14T00:38:25.523Z" },
1588+ { url = "https://files.pythonhosted.org/packages/4b/86/17a85475e218e05225a4f6d65237b12b280596e04520df0e3d8841c4eae3/wrapt-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:df6e3a36170cda0d313be50fe5065948e7f12f3a181b38cbc262e9f2ee4824e1", size = 103415, upload-time = "2026-10-14T00:38:27.409Z" },
1589+ { url = "https://files.pythonhosted.org/packages/27/1c/495b3aebbbe5aebf52ae5f9e8ddd0e072a412b9f5a9bc68e5eba43fa26ba/wrapt-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bc5c0203d383403043fb86c964bd0bab4fcbfb26004ff4bb9c6d02ebc1d608ae", size = 109940, upload-time = "2026-10-14T00:38:29.145Z" },
1590+ { url = "https://files.pythonhosted.org/packages/28/4d/030ecd98da4d052c264290c4fb9f984706c9a19026154c833580e8624a05/wrapt-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a424e8a9776c06aef6313af1d0e3fe6e0838af4241d0c09eb0a3b46f2c9a5ff3", size = 106214, upload-time = "2026-10-14T00:38:30.795Z" },
1591+ { url = "https://files.pythonhosted.org/packages/90/2b/eec5745baaad284fa19232f47796914134e1ea2dd21e289b012aa7981323/wrapt-2.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a18e63910252eb75d8806b4baefbc3a03612502f63eab042e3741b00b719f043", size = 106441, upload-time = "2026-10-14T00:38:32.504Z" },
1592+ { url = "https://files.pythonhosted.org/packages/29/f3/976b0f014a08654289358d41a799c2d24642151b091cfe18a8b91766ee28/wrapt-2.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:183bf0bb893f783c9d22f953cb01fababb9f618e098763f8e66337b575b0647a", size = 106411, upload-time = "2026-10-14T00:38:34.338Z" },
1593+ { url = "https://files.pythonhosted.org/packages/c7/f4/4b94583d9bec0ff0573a5f10fab295675e3b9a64b701c4609b1a1982c390/wrapt-2.5.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1e823aecb3746b8f9e0aee2e1413887871ee2f5c502a3e0ef8d466dbd4adde1", size = 252017, upload-time = "2026-10-14T00:38:36.142Z" },
1594+ { url = "https://files.pythonhosted.org/packages/5e/3c/4f9ba033343b2935a453188f97866f0bd4f7748748ab307aaefb18be3a0a/wrapt-2.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bde5d1b37101b1e9dd3da1f35072e2e7028e9c5e3511f7d76d3fdd4d071b7663", size = 251030, upload-time = "2026-10-14T00:38:38.003Z" },
1595+ { url = "https://files.pythonhosted.org/packages/69/a1/704c761913be404ed893d05702eeda5ff96c5d8448271c28b80101202bcb/wrapt-2.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12d3d2b9d6553df6e2421ab99e1cc5413509076788f57fcb3169f5ce100a19d1", size = 232875, upload-time = "2026-10-14T00:38:40.124Z" },
1596+ { url = "https://files.pythonhosted.org/packages/f0/3a/779ca20fb8c70238069efd0a2b60ea3da450e57c4f103748db0247f52f3a/wrapt-2.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:521bd5ef2a33171fac08a0a302d51a983c19c3519406c1ee8da7ce29285488da", size = 248302, upload-time = "2026-10-14T00:38:42.138Z" },
1597+ { url = "https://files.pythonhosted.org/packages/03/02/80e13786204ce8e1002edb66d06a3194906c0dbbc038bce7bbda426d0f2b/wrapt-2.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:129cab3c7b21e68e693c2819a95c47f3b1c41a834b931154688c83b6aef6bdab", size = 229839, upload-time = "2026-10-14T00:38:44.039Z" },
1598+ { url = "https://files.pythonhosted.org/packages/9d/d1/14c0d041375ae5d0a12445b5c5bc61b109df954cd5bfb852736cd4281cbd/wrapt-2.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8a7c078323e6e1534968cb85488c5eb7ee2b9bbd0f8a291095213a763da40dab", size = 239634, upload-time = "2026-10-14T00:38:45.896Z" },
1599+ { url = "https://files.pythonhosted.org/packages/97/6e/dacc92526fbed1013eb903406ce961da6ff2a1e66b7349a253f439b979aa/wrapt-2.5.1-cp315-cp315-win32.whl", hash = "sha256:736c1de0230c6d24327b14684794214167b2c5ebb6332e28a10f504641b600df", size = 100931, upload-time = "2026-10-14T00:38:47.718Z" },
1600+ { url = "https://files.pythonhosted.org/packages/e8/e4/84bbd88554052958ecbbb481a75559b1e40753aba52ec86e3e1efb50ccf0/wrapt-2.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:69fd0fbb3daf7c8c6f5e062847a0061f880f347374d74cf1daba57220fb64cd0", size = 106252, upload-time = "2026-10-14T00:38:49.511Z" },
1601+ { url = "https://files.pythonhosted.org/packages/b7/98/98d4c4524e8af70ccf35b66864be29ea9d232e5a918efc1dbcf5c87a039d/wrapt-2.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:051220e5071fdfb1a6678707c8abb7bbf4824d40f99758394b2b4d64855fb284", size = 103980, upload-time = "2026-10-14T00:38:51.384Z" },
1602+ { url = "https://files.pythonhosted.org/packages/75/d9/4b242519d6d29eabb73cb9e50e645e014eb2c13f022601151953b3e81946/wrapt-2.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:711e73da3d7983547fc9dd208973b6b0c52640822f5d477910ba24622df6ba64", size = 109538, upload-time = "2026-10-14T00:38:53.124Z" },
1603+ { url = "https://files.pythonhosted.org/packages/58/05/e434f56fcceaafb251cc56c03107ae278e99158466b49f4146f7b33a2532/wrapt-2.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:5be9816d9de88f02fce23cf55f392403411d9bd9c7ae57fdc965a43b22e2de5e", size = 109874, upload-time = "2026-10-14T00:38:54.941Z" },
1604+ { url = "https://files.pythonhosted.org/packages/23/09/d2c0b34d02804018225279a157307b873c8d0f8452790efdd7f0004387a8/wrapt-2.5.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b3f410c416752e1dba53d361e2e6562f22c2c3ec855740dfa5836e061b22571", size = 284866, upload-time = "2026-10-14T00:38:57.081Z" },
1605+ { url = "https://files.pythonhosted.org/packages/71/6f/2b56319c0565d9a6b63eeba2f11f3e699dc324aaa460f1aae079bd4507b0/wrapt-2.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:094b847491b813b6e6c1775e03770930d75078c0821adf929ac712830951ef25", size = 295154, upload-time = "2026-10-14T00:38:59.007Z" },
1606+ { url = "https://files.pythonhosted.org/packages/3c/1b/9ac4238a1a839457d6b687b9e9c35d57ba1b2050a42d6bf8c93176bc1bdf/wrapt-2.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26d8ea2ec6818aeb656bd8a9e745a6f1fb0edfcd8f54291ccd94f62eb5f5e3bd", size = 264914, upload-time = "2026-10-14T00:39:01.154Z" },
1607+ { url = "https://files.pythonhosted.org/packages/4f/95/9faed8e5f6e5431edd36b2cfb4f305df520c197ba3639e1c78d11c70a068/wrapt-2.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a526227efe17dd94bd16b123d170f879bce42c15f10eb92495a745f54caa943", size = 290238, upload-time = "2026-10-14T00:39:03.052Z" },
1608+ { url = "https://files.pythonhosted.org/packages/b5/52/cae26590ef8ee46b55aa8b211c507f6e5ec0fbd241a6730bf7da024b5dab/wrapt-2.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:36d7d0ad593c4f1a651e4032de834db59aee1a929ee396cd483895b673328e51", size = 262208, upload-time = "2026-10-14T00:39:05.008Z" },
1609+ { url = "https://files.pythonhosted.org/packages/00/f3/34e5008307be4169592e99de52946cd8800b98097e2a145da11484c7b3e4/wrapt-2.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:89d9a8607b7028054bb6fd01d437f205534a5d59d53c3665d15949a99a2fce0d", size = 280005, upload-time = "2026-10-14T00:39:07.048Z" },
1610+ { url = "https://files.pythonhosted.org/packages/f3/f9/64e000aa84a88c52a481c7c8b011c80a8ae60bd5f68598e567a918a624a0/wrapt-2.5.1-cp315-cp315t-win32.whl", hash = "sha256:ad81bf81b0a0b6c6ec74169638202851962843e86749570c463eecc55072f93b", size = 103378, upload-time = "2026-10-14T00:39:09.336Z" },
1611+ { url = "https://files.pythonhosted.org/packages/2f/e4/69efa7c6e8535c5188e041ac278079949fb2daaa97e6f08beb91cf31b3d1/wrapt-2.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d5b665a43fe0d3b390cbdd3c003d61c92fa07bd5e3fb1ed3f47920c2d03cd9fd", size = 109928, upload-time = "2026-10-14T00:39:11.156Z" },
1612+ { url = "https://files.pythonhosted.org/packages/13/77/6e414b3388b9f1ecb76107ef4a2aae501f1bdfcab85c8e34ef78f7db22db/wrapt-2.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:6405ff2160af9d59132ebb076eda0304db44d9d09809582932412ef7c0788a36", size = 106226, upload-time = "2026-10-14T00:39:13.061Z" },
1613+ { url = "https://files.pythonhosted.org/packages/bc/0c/7da7513ddcc8f1d831ec4bfbedc9f7f174ecb91042bc16916fc1e0d06b22/wrapt-2.5.1-py3-none-any.whl", hash = "sha256:c6e6c226b1ca5402d7ae5fb34a0d21f1b49124fe4200e5884d1e19e53c47ac1d", size = 81849, upload-time = "2026-10-14T00:39:37.441Z" },
1614+]
1615
Menu

Theme