Technical Guide
How econ-agents works
A closed-loop economic simulation: agent-based consumers and rational suppliers operating in a shared market, driven entirely by YAML configuration.
01 — ArchitectureOverview
The system simulates a single commodity market over a configurable number of discrete rounds. In each round, a population of consumer agents and a pool of supplier agents independently observe the current state of the market, make decisions, and those decisions feed into a market clearing algorithm that sets the next price.
The loop is fully closed: price changes affect consumer behaviour, which affects demand, which affects stock levels, which affects price. Suppliers respond to price signals and commit production orders that arrive after a configurable lag — creating the delayed feedback loops that produce real-world commodity cycles.
Everything is configured in YAML. No code changes are required to run a new scenario — you describe agent populations, supplier economics, price algorithm parameters, and shock schedules, then run the simulation from the command line.
All agent decisions happen concurrently via asyncio.gather. Agents see the same world snapshot and cannot react to each other's decisions within the same round. This is the sealed-bid auction model, not sequential bargaining.
02 — ArchitectureThe Six-Phase Round Loop
Every round executes exactly six phases in sequence. Only phase 3 (Decide) runs concurrently; the rest are sequential.
Scheduled shocks from the YAML config fire here — price shocks, stock destruction events, production cost changes. They alter market state before anyone observes it this round.
A frozen WorldSnapshot is taken: current price, stock level, pipeline totals, price change % from last round. All agents will see this identical snapshot when deciding.
All consumer and supplier coroutines run in parallel via asyncio.gather. Each agent reads the snapshot, evaluates its rules, and returns a decision. Agents cannot observe each other's choices.
Production orders placed in earlier rounds whose arrive_round equals the current round are pulled from the pipeline queue and added to market stock.
Total consumer demand is summed. Actual consumption is min(demand, stock). The shortfall (if any) is logged. The price algorithm then reads the post-consumption stock level and computes next round's price.
Each supplier's new production rate becomes an order scheduled to arrive production_lag rounds from now. The supplier tracks its own pending orders separately for use in next round's observation.
03 — AgentsConsumer Agents
The consumer population is divided into types, each with a proportion of the total population size. Each agent type has a behavior mode and a set of rules expressed in a safe expression DSL.
Behavior Modes
Evaluates rules in order; first matching condition fires. Fully predictable given the same market state.
No rules — picks an action by weighted random draw from a stochastic_actions list each round.
Rules are checked first. If none match, falls back to the stochastic weighted draw. Best of both: panic rules override random noise.
The Rule DSL
Conditions are Python-compatible arithmetic expressions evaluated against a per-agent observation dict. The evaluator is a safe AST walker — no eval(), no builtins, only arithmetic operators and comparisons.
# Example rule in a YAML config
- condition: "price_change_pct > 15 or stock_ratio < 0.5"
action: buy_more
quantity_factor: 3.0
label: "panic hoard: big spike or severe shortage"
Observation Variables
| Variable | Meaning |
|---|---|
| price | Current market price this round |
| price_change_pct | % change vs previous round (negative = falling) |
| stock_ratio | current_stock / target_stock (1.0 = at buffer, <1 = shortage) |
| stock | Absolute units in warehouse |
| pipeline_total | Total units in the market's production pipeline (not yet arrived) |
| round | Current round number |
Quantity Demanded
Each action (buy_more, buy_less, hold, no_change) maps to a quantity_factor multiplied against the agent's base_quantity. hold means zero demand this round. no_change means exactly base_quantity. Total market demand is the sum across all agents.
04 — AgentsSupplier Agents
Suppliers are rational producers who observe market price, their own production cost, and market/pipeline conditions to decide whether to expand or contract output.
Rate Adjustment
Each round, a supplier evaluates its rules and selects an adjustment_factor (a signed fraction, e.g. 0.10 for +10%). The new rate is:
adj = clamp(raw_adj, −max_ramp_down, +max_ramp_up)
The min_production_rate floor prevents the multiplicative zero-trap: a supplier that cuts to 0 via normal rules can never recover through further rate multiplication. Only bankruptcy (rate forced to exactly 0) bypasses the floor.
Production Lag
Orders placed in phase 6 arrive exactly production_lag rounds later. A supplier with lag=5 committing to a new rate today won't see that output land in the market until round N+5. This is the primary driver of commodity cycle dynamics — suppliers over-ramp relative to what the market actually needs, because the lag obscures the demand signal.
Own Pipeline Tracking
Each supplier tracks its own pending orders separately from the market-wide pipeline. The observation variable own_pipeline_rounds tells a supplier how many rounds-worth of current-rate production it already has in flight. This prevents double-ordering without relying on the market aggregate (which mixes all suppliers).
During a ramp-up phase, own_pipeline_rounds always converges to slightly below the production lag — because older orders in the pipeline were placed at lower (pre-ramp) rates. A caution threshold above the lag can never trigger during steady ramping. Place stock_ratio-based cut rules before generic margin-expansion rules to guard against this.
Supplier Observation Variables
| Variable | Meaning |
|---|---|
| margin | price / production_cost (1.0 = break-even) |
| price_change_pct | % change in market price vs last round |
| stock_ratio | market stock / target stock |
| own_pipeline_total | Total units this supplier has in transit |
| own_pipeline_rounds | own_pipeline_total / current_rate |
| market_pipeline_total | All units in the market pipeline (all suppliers) |
| consecutive_loss_rounds | Rounds this supplier has operated with margin < 1.0 |
| bankrupt | 1.0 if bankrupt (rate forced to 0), else 0.0 |
| current_production_rate | Current rate before this round's adjustment |
Bankruptcy Mechanic
When a supplier operates below its production cost (margin < 1.0), it accumulates consecutive_loss_rounds. After bankruptcy_threshold rounds of sustained losses, it goes bankrupt: rate is forced to zero and no further orders are placed.
Rules fire normally. Rate bounded below by min_production_rate.
consecutive_loss_rounds increments. Pre-bankruptcy rules can fire emergency cuts (e.g. consecutive_loss_rounds >= N-1).
Supplier exits: bankrupt = True, rate → 0. No orders placed.
Each round, the supplier checks if margin >= restart_margin. If yes: bankrupt = False, rate restarts at restart_rate, loss streak resets.
05 — MarketPrice Algorithm & Clearing
Stock-Based Pricing
The only implemented price algorithm is stock_based. After each round's consumption, the remaining stock is compared to a target buffer. The price moves to reflect scarcity:
Δprice = −elasticity × ln(stock / target_stock)
new_price = clamp(old_price × eΔprice, min_price, max_price)
demand_avg is a rolling average over the last demand_window rounds, making the target adaptive. When stock < target_stock, the log term is negative, Δprice is positive — price rises. When stock exceeds target, price falls. The elasticity parameter controls how aggressively price responds to the imbalance.
Market Clearing
At the end of phase 5, demand is settled against whatever stock is on hand after pipeline deliveries:
shortage = max(0, demand − actual_consumption)
fill_rate = actual_consumption / demand
stock_after = stock_after_harvest − actual_consumption
No partial rationing model is applied — demand is either filled or it isn't. The shortage is reported but does not directly affect agent decisions in the same round (agents see the pre-clearing snapshot).
Shock Types
| Type | Effect | Example |
|---|---|---|
| price_shock | Multiplies current price by (1 + delta_pct/100) before observe phase | +25% → price × 1.25 |
| stock_shock | Multiplies current stock by (1 + delta_pct/100) | −40% → removes 40% of warehouse |
| cost_shock | Multiplies market's blended production_cost; also propagates to all supplier agents' individual costs | +10% → all suppliers more expensive |
06 — ConfigurationYAML Structure
A scenario is a single YAML file. The top-level keys are:
seed: 42 # RNG seed for reproducible stochastic runs
rounds: 50 # how many rounds to simulate
markets:
rice: # market name (referenced by agents)
initial_price: 1.00
initial_stock: 5000
production_cost: 0.65
price_algorithm:
type: stock_based
target_stock_days: 10
elasticity: 0.25
demand_window: 3
min_price: 0.20
max_price: 4.00
suppliers:
- id: large_producer
count: 2 # number of agents of this type
market: rice
production_cost: 0.60
initial_production_rate: 300
production_lag: 5
max_ramp_up: 0.10
max_ramp_down: 0.20
bankruptcy_threshold: 8
restart_rate: 30
restart_margin: 1.35
min_production_rate: 15
rules:
- condition: "stock_ratio > 2.0"
action: decrease_production
adjustment_factor: -0.10
label: "glut — cut"
consumers:
size: 1000
agent_types:
- id: hoarder
proportion: 0.20 # fraction of total size
mode: deterministic
base_quantity: 1.0
rules:
- condition: "price_change_pct > 8"
action: buy_more
quantity_factor: 2.0
label: "hoard on spike"
shocks:
- round: 6
type: stock_shock
market: rice
delta_pct: -20.0
description: "Flood destroys 20% of warehouse stock"
Key calibration tips
Baseline equilibrium. Set initial supply ≈ baseline demand. With 1000 consumers at base_quantity: 1.0, baseline demand is roughly 700–900 (stochastic + hold actions reduce it). Total supplier count × initial_production_rate should match this.
Target stock buffer. target_stock = target_stock_days × demand_avg. Set initial_stock near this value to start at a neutral price. Starting below target makes the price spike immediately; starting above makes it fall.
Rule ordering matters. Rules are first-match. Put glut-detection cut rules before margin-expansion rules, or a profitable supplier will keep ramping even into a glutted market.
07 — CLIRunning Scenarios
All flags can be combined. The config path is the only positional argument and defaults to configs/rice_price.yaml.
08 — DynamicsWhat to Watch For
The Cobweb / Commodity Cycle
The most reliable emergent pattern. A supply shock causes prices to spike. Suppliers ramp output, but their orders don't land until several rounds later. By the time the new supply arrives, other suppliers have also ramped — a glut forms, price collapses, suppliers exit or go bankrupt, and the cycle begins again. Longer production lags produce more violent cycles.
The Bullwhip Effect
Consumer demand variability amplifies as it travels upstream. A 10% demand spike causes suppliers to over-order by a much larger factor, because each supplier is also guarding against the worst case and the lag makes the demand signal stale. Scenarios with long lags (semiconductor, wheat) show this most vividly.
Supplier Asymmetry
When supplier types have different cost structures and bankruptcy thresholds, the glut phase becomes a shakeout. High-cost suppliers go bankrupt first; low-cost suppliers weather the floor price. After the shakeout, the surviving concentrated market can sustain a permanent price above pre-crisis levels (the oil_glut scenario demonstrates this).
Reading the Summary Table
| Signal | What it means |
|---|---|
| Price at max_price for many rounds | Supply chain crisis — stock exhausted, no relief from pipeline yet |
| Price at min_price + growing stock | Glut phase — suppliers are cutting but pipeline orders still arriving |
| fill_rate < 100% | Demand exceeded supply — rationing occurred, shortage counted |
| Supplier action = bankrupt | Capital exhausted; that supplier is idle until margin recovers |
| Supplier action = restart | Re-entry at restart_rate — new cycle beginning |
| Supplier action = maintain (many rounds) | Pipeline caution firing — or no rule matched |
| stock_ratio ≫ 1 and price rising | Demand shock overriding the glut signal — check for recent price_shock |
rice_price.yaml — baseline 20-round demonstration · wheat_crisis.yaml — export ban, 50 rounds · semiconductor_shortage.yaml — chip cycle with AI supercycle restart, 120 rounds · oil_glut.yaml — OPEC vs shale shakeout, 55 rounds · coffee_harvest_failure.yaml — sequential crop failures, 55 rounds