Skip to main content

Market Simulation

The AEX market simulator (aex-sim) populates your exchange with AI-driven trading personas that generate realistic order flow, pricing behaviour, and market dynamics. Each persona is backed by an LLM strategy advisor that periodically reviews market conditions and adjusts trading decisions based on its role and risk profile.

The MCP server exposes five tools for controlling simulations from a Claude Code session.

How It Works

When you start a simulation, each persona:

  1. Connects to AEX as a real registered user (using your AEX user IDs)
  2. Periodically evaluates market conditions — prices, positions, credit utilisation
  3. Submits limit orders based on its type, generation pattern, and LLM guidance
  4. Responds to fills and market events by adjusting its strategy

All personas trade simultaneously. The market sees them as ordinary participants — their orders appear in the order book, match with real orders, and generate fills.

note

Each persona must map to an existing AEX entity and user. Use aex_list_entities and aex_list_users to find valid IDs before starting a simulation.

Persona Types

TypeRoleBehaviour
retailerElectricity buyerPurchases power for consumption; price-sensitive, lower risk appetite
solar_generatorSolar generationSells during daylight hours; follows solar generation pattern
baseload_generatorThermal/hydro generationSteady seller; large volumes, low price tolerance, long review cycles
wind_generatorWind generationSells with variable output; higher volatility tolerance
speculatorFinancial traderTrades both sides; short review cycles, higher LLM temperature
industrialLarge consumerBulk buyer; large order sizes, infrequent reviews
market_makerSpread quotingContinuously quotes two-way prices; maintains spread and reloads after fills

Generation Patterns

Personas with a physical profile use a pattern to shape their trading activity over time:

PatternDescription
constantUniform activity regardless of time of day (default)
solarActivity follows a solar generation curve — peaks at midday
windStochastic output with random variation
peak_demandActive during morning and evening peak demand windows
flat_demandUniform consumption across all hours

Scenarios (Spot Price Model)

When you pass a scenario, the simulator enables an internal spot price model that drives persona decisions based on realistic NZ electricity market conditions. Without a scenario, personas trade on the live AEX order book alone.

ScenarioDescription
normal_wetAverage hydro storage, wet conditions — lower prices
normalTypical market conditions, balanced supply/demand
dry_yearReduced hydro availability — elevated prices, higher volatility
calm_summerLow demand, high renewable output — suppressed prices

Starting a Simulation

Call aex_sim_start with an array of persona configurations:

{
"personas": [
{
"type": "baseload_generator",
"name": "Meridian",
"entityId": "MERIDIAN",
"userId": "azure-ad-object-id-of-meridian-user"
},
{
"type": "retailer",
"name": "Genesis",
"entityId": "GENESIS",
"userId": "azure-ad-object-id-of-genesis-user"
},
{
"type": "market_maker",
"name": "Transpower MM",
"entityId": "TRANSPOWER",
"userId": "azure-ad-object-id-of-transpower-user",
"spreadWidthCents": 150,
"quoteSizeMW": 10
}
],
"scenario": "normal",
"llmEnabled": true,
"verbose": false
}

aex_sim_start Parameters

ParameterTypeRequiredDefaultDescription
personasarrayYesArray of persona configuration objects (see below).
scenariostringNoSpot model scenario: normal_wet, normal, dry_year, calm_summer. Enables the spot price model when set.
aexHoststringNolocalhost:8080AEX WebSocket host.
redisHoststringNolocalhostRedis host.
redisPortnumberNo6379Redis port.
llmEnabledbooleanNotrueEnable LLM strategy advisor. Set to false for deterministic rule-based trading.
llmModelstringNoclaude-sonnet-4-20250514Anthropic model for the LLM advisor.
verbosebooleanNofalseEnable verbose logging to stderr.
seednumberNo0 (random)Random seed for reproducible simulations.
maxOrdersPerMinutenumberNo120Circuit breaker: maximum orders per minute across all personas.
msPerTradingPeriodnumberNo30000Real milliseconds per simulated 30-minute trading period. Only applies when scenario is set.

Persona Configuration

Each persona object in the personas array accepts:

ParameterTypeRequiredDefaultDescription
typestringYesPersona type (see Persona Types).
namestringYesDisplay name for this persona.
entityIdstringYesAEX entity ID this persona trades as.
userIdstringYesAzure AD Object ID of the AEX user.
capacityMWnumberNo50Physical generation or consumption capacity in MW.
patternstringNoconstantGeneration/consumption pattern (see Generation Patterns).
maxPositionMWnumberNo100Maximum net position in MW (risk limit).
tickIntervalMsnumberNo5000How often the persona evaluates and potentially trades (milliseconds).
profilesstring[]No["BASE", "PEAK"]Contract profiles to trade (e.g. BASE, PEAK, ON, MP, EP).
nodesstring[]No["OTA", "BEN"]GXP nodes to trade at.
spreadWidthCentsnumberNo200Two-way spread width in cents. Market maker only.
quoteSizeMWnumberNo10Volume in MW quoted on each side of the spread. Market maker only.
reloadSizeMWnumberNo10Volume in MW to re-quote after a fill. Market maker only.

Returns: { status, message, personas, spotModel }


Stopping a Simulation

aex_sim_stop

No parameters required. Cancels all outstanding orders, disconnects all personas, and returns a summary:

{
"status": "stopped",
"summary": {
"uptime": 142,
"totalOrders": 87,
"totalFills": 23,
"llmCalls": 14
}
}

Checking Simulation Status

aex_sim_status

Returns the current state of all personas including their active positions, order counts, and the latest LLM guidance each received:

{
"status": "running",
"uptime": "67s",
"metrics": {
"totalOrders": 45,
"totalFills": 12,
"llmCalls": 8
},
"personas": [
{
"name": "Meridian",
"type": "baseload_generator",
"entityId": "MERIDIAN",
"running": true,
"activeOrders": 3,
"totalOrders": 18,
"totalFills": 5,
"positions": { "OTA_BASE": 40, "BEN_BASE": 30 },
"lastGuidance": {
"bias": "sell",
"appetite": "moderate",
"reasoning": "Hydro storage adequate, targeting base load sales at current price levels"
}
}
]
}

Spot Prices

When running with a scenario, aex_sim_get_spot_prices returns the current simulated spot prices for both BEN and OTA nodes across all contract profiles:

aex_sim_get_spot_prices

Returns:

{
"status": "ok",
"period": 14,
"month": 4,
"season": "autumn",
"regime": "normal",
"activeEvents": [],
"prices": {
"BEN": {
"BASE": "$85.20/MWh (8520c)",
"PEAK": "$142.50/MWh (14250c)",
"ON": "$95.00/MWh (9500c)"
},
"OTA": {
"BASE": "$87.40/MWh (8740c)",
"PEAK": "$145.80/MWh (14580c)",
"ON": "$97.20/MWh (9720c)"
}
}
}

Requires the simulation to be running with a scenario set. Returns an error if the spot model is not enabled.


Triggering Market Events

Inject a synthetic market event into the spot price model to test how personas respond:

{
"type": "hvdc_failure",
"duration": 8
}

aex_sim_trigger_event Parameters

ParameterTypeRequiredDescription
typestringYesEvent type to trigger (see table below).
durationnumberNoDuration override in trading periods. Uses a randomised default if omitted.

Event Types

EventEffect
thermal_outageOTA prices ×1.8 — simulates loss of thermal generation
hvdc_failureOTA prices ×3.5, BEN prices ×0.85 — simulates HVDC link failure (classic NZ island separation)
dry_spellLong-duration price increase — simulates reduced hydro inflows
wet_spellLong-duration price decrease — simulates high hydro storage
demand_surgeShort price spike — simulates unexpected demand increase
wind_eventShort price decrease — simulates high wind generation

Returns:

{
"status": "triggered",
"event": {
"type": "hvdc_failure",
"benMultiplier": 0.85,
"otaMultiplier": 3.5,
"remainingPeriods": 6,
"regimeOverride": "high"
},
"message": "Event 'hvdc_failure' triggered. Duration: 6 periods. Regime override: high."
}

Requires the simulation to be running with a scenario set (spot model enabled).


Example: Full Simulation Workflow

# 1. Check market is open
aex_get_market_state

# 2. Discover available entities and users
aex_list_entities
aex_list_users

# 3. Start a simulation with three personas
aex_sim_start {
"personas": [
{ "type": "baseload_generator", "name": "Hydro Co", "entityId": "HYDRO", "userId": "..." },
{ "type": "retailer", "name": "Retailer A", "entityId": "RETAIL", "userId": "..." },
{ "type": "market_maker", "name": "MM1", "entityId": "MMAKER", "userId": "..." }
],
"scenario": "dry_year"
}

# 4. Watch the order book fill
aex_get_order_book
aex_watch_orders

# 5. Check simulation progress
aex_sim_status
aex_sim_get_spot_prices

# 6. Trigger a stress event
aex_sim_trigger_event { "type": "hvdc_failure" }

# 7. Observe how prices and order flow react
aex_sim_get_spot_prices
aex_watch_book

# 8. Stop when done
aex_sim_stop

Next Steps