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>
113 changed files +10,847 −0
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 | |||||