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:
- Connects to AEX as a real registered user (using your AEX user IDs)
- Periodically evaluates market conditions — prices, positions, credit utilisation
- Submits limit orders based on its type, generation pattern, and LLM guidance
- 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.
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
| Type | Role | Behaviour |
|---|---|---|
retailer | Electricity buyer | Purchases power for consumption; price-sensitive, lower risk appetite |
solar_generator | Solar generation | Sells during daylight hours; follows solar generation pattern |
baseload_generator | Thermal/hydro generation | Steady seller; large volumes, low price tolerance, long review cycles |
wind_generator | Wind generation | Sells with variable output; higher volatility tolerance |
speculator | Financial trader | Trades both sides; short review cycles, higher LLM temperature |
industrial | Large consumer | Bulk buyer; large order sizes, infrequent reviews |
market_maker | Spread quoting | Continuously 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:
| Pattern | Description |
|---|---|
constant | Uniform activity regardless of time of day (default) |
solar | Activity follows a solar generation curve — peaks at midday |
wind | Stochastic output with random variation |
peak_demand | Active during morning and evening peak demand windows |
flat_demand | Uniform 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.
| Scenario | Description |
|---|---|
normal_wet | Average hydro storage, wet conditions — lower prices |
normal | Typical market conditions, balanced supply/demand |
dry_year | Reduced hydro availability — elevated prices, higher volatility |
calm_summer | Low 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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
personas | array | Yes | — | Array of persona configuration objects (see below). |
scenario | string | No | — | Spot model scenario: normal_wet, normal, dry_year, calm_summer. Enables the spot price model when set. |
aexHost | string | No | localhost:8080 | AEX WebSocket host. |
redisHost | string | No | localhost | Redis host. |
redisPort | number | No | 6379 | Redis port. |
llmEnabled | boolean | No | true | Enable LLM strategy advisor. Set to false for deterministic rule-based trading. |
llmModel | string | No | claude-sonnet-4-20250514 | Anthropic model for the LLM advisor. |
verbose | boolean | No | false | Enable verbose logging to stderr. |
seed | number | No | 0 (random) | Random seed for reproducible simulations. |
maxOrdersPerMinute | number | No | 120 | Circuit breaker: maximum orders per minute across all personas. |
msPerTradingPeriod | number | No | 30000 | Real milliseconds per simulated 30-minute trading period. Only applies when scenario is set. |
Persona Configuration
Each persona object in the personas array accepts:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | — | Persona type (see Persona Types). |
name | string | Yes | — | Display name for this persona. |
entityId | string | Yes | — | AEX entity ID this persona trades as. |
userId | string | Yes | — | Azure AD Object ID of the AEX user. |
capacityMW | number | No | 50 | Physical generation or consumption capacity in MW. |
pattern | string | No | constant | Generation/consumption pattern (see Generation Patterns). |
maxPositionMW | number | No | 100 | Maximum net position in MW (risk limit). |
tickIntervalMs | number | No | 5000 | How often the persona evaluates and potentially trades (milliseconds). |
profiles | string[] | No | ["BASE", "PEAK"] | Contract profiles to trade (e.g. BASE, PEAK, ON, MP, EP). |
nodes | string[] | No | ["OTA", "BEN"] | GXP nodes to trade at. |
spreadWidthCents | number | No | 200 | Two-way spread width in cents. Market maker only. |
quoteSizeMW | number | No | 10 | Volume in MW quoted on each side of the spread. Market maker only. |
reloadSizeMW | number | No | 10 | Volume 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
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Event type to trigger (see table below). |
duration | number | No | Duration override in trading periods. Uses a randomised default if omitted. |
Event Types
| Event | Effect |
|---|---|
thermal_outage | OTA prices ×1.8 — simulates loss of thermal generation |
hvdc_failure | OTA prices ×3.5, BEN prices ×0.85 — simulates HVDC link failure (classic NZ island separation) |
dry_spell | Long-duration price increase — simulates reduced hydro inflows |
wet_spell | Long-duration price decrease — simulates high hydro storage |
demand_surge | Short price spike — simulates unexpected demand increase |
wind_event | Short 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
- MCP Tool Reference — complete parameter reference for all tools
- MCP Server Overview — connection model and environment variable setup