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.

Key design choice

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.

SHOCKphase 1
Apply exogenous events

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.

OBSERVEphase 2
Snapshot the world

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.

DECIDEphase 3
Agents decide — concurrently

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.

HARVESTphase 4
Pipeline deliveries arrive

Production orders placed in earlier rounds whose arrive_round equals the current round are pulled from the pipeline queue and added to market stock.

CLEARphase 5
Demand meets stock; price is set

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.

PLACEphase 6
Supplier orders enter the pipeline

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

deterministic

Evaluates rules in order; first matching condition fires. Fully predictable given the same market state.

stochastic

No rules — picks an action by weighted random draw from a stochastic_actions list each round.

mixed

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

VariableMeaning
priceCurrent market price this round
price_change_pct% change vs previous round (negative = falling)
stock_ratiocurrent_stock / target_stock (1.0 = at buffer, <1 = shortage)
stockAbsolute units in warehouse
pipeline_totalTotal units in the market's production pipeline (not yet arrived)
roundCurrent 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:

Rate Update
new_rate = max(min_production_rate, old_rate × (1 + adj))
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).

Pipeline caution rules have a known limitation

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

VariableMeaning
marginprice / production_cost (1.0 = break-even)
price_change_pct% change in market price vs last round
stock_ratiomarket stock / target stock
own_pipeline_totalTotal units this supplier has in transit
own_pipeline_roundsown_pipeline_total / current_rate
market_pipeline_totalAll units in the market pipeline (all suppliers)
consecutive_loss_roundsRounds this supplier has operated with margin < 1.0
bankrupt1.0 if bankrupt (rate forced to 0), else 0.0
current_production_rateCurrent 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.

Normal operation

Rules fire normally. Rate bounded below by min_production_rate.

Below cost — loss streak begins

consecutive_loss_rounds increments. Pre-bankruptcy rules can fire emergency cuts (e.g. consecutive_loss_rounds >= N-1).

Bankruptcy threshold crossed

Supplier exits: bankrupt = True, rate → 0. No orders placed.

Market recovery

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 Update (stock_based)
target_stock = demand_avg × target_stock_days

Δ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:

Clearing
actual_consumption = min(demand, stock_after_harvest)
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

TypeEffectExample
price_shockMultiplies current price by (1 + delta_pct/100) before observe phase+25% → price × 1.25
stock_shockMultiplies current stock by (1 + delta_pct/100)−40% → removes 40% of warehouse
cost_shockMultiplies 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

$ python main.py configs/rice_price.yaml # rich live display
$ python main.py configs/wheat_crisis.yaml --no-viz # plain text output
$ python main.py configs/semiconductor_shortage.yaml --rounds 60 # override rounds
$ python main.py configs/coffee_harvest_failure.yaml --export-gif coffee.gif # animated 4-panel GIF
$ python main.py configs/oil_glut.yaml --output-json oil.json # full decision log
$ python main.py configs/rice_price.yaml --delay 0.5 # slow down live display

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

SignalWhat it means
Price at max_price for many roundsSupply chain crisis — stock exhausted, no relief from pipeline yet
Price at min_price + growing stockGlut phase — suppliers are cutting but pipeline orders still arriving
fill_rate < 100%Demand exceeded supply — rationing occurred, shortage counted
Supplier action = bankruptCapital exhausted; that supplier is idle until margin recovers
Supplier action = restartRe-entry at restart_rate — new cycle beginning
Supplier action = maintain (many rounds)Pipeline caution firing — or no rule matched
stock_ratio ≫ 1 and price risingDemand shock overriding the glut signal — check for recent price_shock
Included scenarios

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