Simulations
Run agent-based market simulations and retrieve results.
All simulation methods are available on client.simulation. What you can do depends on your tier.
Free tier
- List cached simulations with
list_cached() - Download data with
get_sim_data(sim_id)
Pro tier
- Submit a job with
run()— returns a job_id - Track progress with
get_job_status(job_id) - Find past jobs with
get_jobs() - Retrieve results with
get_sim_data()
Free tier
Cached simulations
Free tier users can browse and download a fixed set of pre-run baseline simulations across a selection of HKEX and LSE symbols, calibration dates, and scenarios, with up to 10 Monte Carlo runs per group.
List cached simulations
# List all available cached simulations
cached = client.simulation.list_cached()
print(f"Found {cached['total']} simulation groups")
for sim in cached["simulations"]:
print(f"{sim['symbol']} {sim['date']} {sim['scenario']}: {sim['n_runs']} runs")
print(f" sim_id: {sim['example_sim_id']}")
# Filter by symbol
cached = client.simulation.list_cached(symbol="700.HK")
# Filter by scenario
cached = client.simulation.list_cached(scenario="flash_crash")Download cached simulation data
# Find a cached simulation
cached = client.simulation.list_cached(symbol="700.HK", scenario="normal")
sim_id = cached["simulations"][0]["example_sim_id"]
# Download full simulation data
df = client.simulation.get_sim_data(sim_id)
print(df.head())
# Download mid-price series
mid_df = client.simulation.get_sim_data(sim_id, "mid_price_by_min.parquet")Download limits
Free-tier downloads are metered per simulation group — all the Monte Carlo runs of one symbol/date/scenario combination. You can add up to 3 new groups per rolling 24-hour window by default, counted the same way on every download path: single-file downloads (get_sim_data), bulk ZIPs (get_bulk_data), and the web explorer. Not per call, per run, or per file format.
Once you have downloaded a group it is yours permanently: re-fetching it later — more runs, a different file format, a different endpoint — never consumes quota again. Pro and demo tiers are unlimited.
Check your current allowance and the groups you own with client.profile.downloads():
quota = client.profile.downloads()
print(quota){"limit": 3, "used": 1, "remaining": 2, "window_hours": 24,
"downloaded_groups": ["omd:hkex_securities:700.HK:2025-09-02:...:baseline:0000"]}used counts new groups added in the current window; downloaded_groups lists every group you have ever downloaded (all free to re-fetch). limit and remaining are null on the pro and demo tiers (unlimited). A request that would exceed the allowance returns HTTP 429 without charging anything, until the window rolls over. If you need more than the default allowance, email support@simudyne.com — we can raise your daily limit without a Pro upgrade.
Pro tier
Submit a simulation
Use run() to submit a custom simulation job. Jobs run asynchronously and return a job ID immediately.
An instrument is identified by four fields: provider (data provider, e.g. omd), exchange (exchange protocol name, e.g. hkex_securities), symbol, and cal_date. All four are required when submitting a simulation.
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5
)
job_id = result["job_id"]
sim_ids = result["queued_sim_ids"]
print(f"Submitted job {job_id} with {len(sim_ids)} simulations")Required parameters
| Parameter | Type | Description |
|---|---|---|
| symbol | str | Trading symbol (e.g., 9999.HK, 0005.HK) |
| cal_date | str | Calibration date in YYYY-MM-DD format |
| provider | str | Data provider, e.g. omd |
| exchange | str | Exchange protocol name, e.g. hkex_securities |
Optional parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| n_runs | int | 100 | Number of Monte Carlo runs |
| seed | int | 42 | Random seed for reproducibility |
| scenario | str | normal | Market scenario to simulate |
| scenario_params | dict | None | Override scenario defaults |
| exec_algos | list | None | Execution algorithms to test |
Finding calibration dates
Use get_available_symbols() to discover valid symbol and date combinations. Only dates where status = "complete" and stage = "model_calibration" will work — any other date will fail. See Available symbols for the full response schema and filtering example.
Market scenarios
Inject market events into your simulation to stress-test strategies. Set the scenario parameter to one of:
| Scenario | Description |
|---|---|
| normal | No injection — background agents only (default) |
| flash_crash | Large rapid SELL depleting bid-side liquidity |
| buy_panic | Large rapid BUY depleting ask-side liquidity |
| gradual_selloff | Slow sustained SELL over extended period |
| trending_up | Small steady BUY producing persistent uptrend |
| trending_down | Small steady SELL producing persistent downtrend |
Example: Flash crash
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
scenario="flash_crash",
scenario_params={
"start_time": "11:00:00",
"impact_multiplier": 15.0
}
)
job_id = result["job_id"]Scenario parameters
All four parameters can be overridden for any non-normal scenario. Defaults differ by scenario — see the per-scenario defaults table below.
| Parameter | Type | Description |
|---|---|---|
| impact_multiplier | float | Total scenario volume as a multiple of average resting liquidity |
| order_size_ratio | float | Size of each child order as a fraction of average resting liquidity |
| order_freq | str | pandas-compatible duration string for child order spacing (e.g. 500ms, 5s, 30s) |
| start_time | str (HH:MM:SS) | Wall-clock time to begin scenario injection (e.g. 10:30:00) |
Scenario parameter defaults
| Scenario | impact_multiplier | order_size_ratio | order_freq | start_time |
|---|---|---|---|---|
| flash_crash | 22.0 | 0.19 | 500ms | 10:30:00 |
| buy_panic | 22.0 | 0.19 | 500ms | 10:30:00 |
| gradual_selloff | 10.0 | 0.05 | 5s | 10:30:00 |
| trending_up | 5.0 | 0.03 | 30s | 10:30:00 |
| trending_down | 5.0 | 0.03 | 30s | 10:30:00 |
Execution algorithms
Test execution strategies by passing an exec_algos list. Each algorithm config must include a type key.
TWAP (Time-Weighted Average Price)
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
exec_algos=[{
"type": "twap",
"order_size": 50, # Total volume to execute, in lots
"horizon": 7200, # Execution window in seconds (2 hours)
"start_time": "09:30:00", # Optional, defaults to market open
"frequency": 30, # Optional, interval in seconds (default 1)
"side": "sell", # Optional, inferred from order_size sign
"random_offset": True # Optional, default True
}]
)
job_id = result["job_id"]VWAP (Volume-Weighted Average Price)
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
exec_algos=[{
"type": "vwap",
"order_size": 100000,
"horizon": 7200, # Execution window in seconds (2 hours)
"start_time": "10:00:00",
"frequency": 30, # Interval in seconds
"random_offset": True
}]
)
job_id = result["job_id"]CSS (Custom Static Schedule)
The orders parameter must be a pd.Series with a full datetime index — date and time combined. Use datetime.combine(cal_date, t) to build the start and end datetimes, then pd.date_range(..., periods=n) to space orders evenly across the window. Orders placed outside HKEX continuous trading hours (09:30–12:00, 13:00–16:00) are silently dropped.
import numpy as np
import pandas as pd
from datetime import date, datetime
def make_schedule(total_shares, start_t, end_t, cal_date, target_clip=100):
"""Distribute total_shares evenly across a datetime window."""
start_dt = datetime.combine(cal_date, start_t)
end_dt = datetime.combine(cal_date, end_t)
n_orders = int(np.ceil(total_shares / target_clip))
idx = pd.date_range(start=start_dt, end=end_dt, periods=n_orders)
# Spread remainder across first orders so total is exact
q, r = divmod(total_shares, n_orders)
sizes = np.full(n_orders, q, dtype=int)
sizes[:r] += 1
return pd.Series(sizes, index=idx)
cal_date = date(2025, 9, 1)
schedule = make_schedule(
total_shares=10_000,
start_t=datetime.strptime("09:30", "%H:%M").time(),
end_t=datetime.strptime("12:00", "%H:%M").time(),
cal_date=cal_date,
)
print(schedule)
print(f"Orders: {len(schedule)}, Total shares: {int(schedule.sum())}")2025-09-02 09:30:00.000000000 100
2025-09-02 09:31:30.909090909 100
2025-09-02 09:33:01.818181818 100
2025-09-02 09:34:32.727272727 100
2025-09-02 09:36:03.636363636 100
...
2025-09-02 11:56:57.272727272 100
2025-09-02 11:58:28.181818181 100
2025-09-02 11:59:59.090909090 100
2025-09-02 12:00:00.000000000 100
dtype: int64
Orders: 100, Total shares: 10000import pandas as pd
from datetime import date, datetime, time
# CSS takes an explicit schedule: shares to place at each timestamp
cal_date = date(2025, 9, 1)
idx = pd.date_range(
datetime.combine(cal_date, time(9, 30)),
datetime.combine(cal_date, time(12, 0)),
periods=100,
)
schedule = pd.Series(100, index=idx) # 100 shares per clip
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
exec_algos=[{
"type": "css",
"orders": schedule
}]
)
job_id = result["job_id"]Multiple CSS strategies in one job
Pass multiple CSS configs in a single exec_algos list to compare strategies against the same shared baseline. Each strategy gets its own set of Monte Carlo runs within the same job.
import numpy as np
import pandas as pd
from datetime import date, datetime, time
def make_schedule(total_shares, start_t, end_t, cal_date, target_clip=100):
"""Distribute total_shares evenly across a datetime window."""
start_dt = datetime.combine(cal_date, start_t)
end_dt = datetime.combine(cal_date, end_t)
n_orders = int(np.ceil(total_shares / target_clip))
idx = pd.date_range(start=start_dt, end=end_dt, periods=n_orders)
q, r = divmod(total_shares, n_orders)
sizes = np.full(n_orders, q, dtype=int)
sizes[:r] += 1
return pd.Series(sizes, index=idx)
cal_date = date(2025, 9, 1)
# Build a grid of execution windows to compare
windows = [
(time(9, 30), time(12, 0)), # full morning
(time(9, 30), time(16, 0)), # full day
(time(13, 0), time(16, 0)), # full afternoon
]
exec_algos = [
{"type": "css", "orders": make_schedule(10_000, s, e, cal_date)}
for s, e in windows
]
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
exec_algos=exec_algos,
)
job_id = result["job_id"]
# sim_ids are ordered: baseline[0:n_runs], strategy_0[n_runs:2*n_runs], ...
sim_ids = result["queued_sim_ids"]Execution algorithm parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| type | str | Yes | Algorithm type: twap, vwap, or css |
| order_size | int | twap/vwap | Total volume to execute |
| horizon | int (seconds) | twap/vwap | Execution window duration |
| orders | pd.Series | css | Order schedule indexed by datetime |
| start_time | str (HH:MM:SS) | No | Start time, defaults to market open |
| frequency | int (seconds) | No | Order frequency, default 1 second |
| side | str | No | buy or sell. If omitted, inferred from sign of order_size (twap/vwap) or orders values (css) |
| random_offset (twap/vwap) | bool | No | Randomise order times within each frequency interval. Default: true |
| random_offset (css) | number (seconds) | No | Jitter window — each order is offset by a random amount up to this many seconds. Default: null (disabled) |
Check job status
Use get_job_status() to track simulation progress. Jobs move through: queued → running → completed (or error).
job_id = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
)["job_id"]
status = client.simulation.get_job_status(job_id)
print(f"Total: {status['total_simulations']}")
print(f"Status: {status['status_summary']}")
print(f"Complete: {status['is_complete']}")Total: 5
Status: {'complete': 3, 'running': 2, 'failed': 0}
Complete: FalsePolling for completion
import time
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5
)
job_id = result["job_id"]
sim_ids = result["queued_sim_ids"]
while True:
status = client.simulation.get_job_status(job_id)
summary = status.get("status_summary", {})
completed = summary.get("complete", 0) + summary.get("completed", 0)
total = status.get("total_simulations", len(sim_ids))
print(f"
{completed}/{total} complete", end="", flush=True)
if status.get("is_complete") or status.get("has_errors"):
break
time.sleep(30) # Check every 30 secondsView past jobs
Use get_jobs() to list all your simulation jobs. Useful for retrieving job IDs from previous sessions.
result = client.simulation.get_jobs()
print(f"You have {result['total']} jobs")
for job in result["jobs"]:
print(f"Job {job['job_id']}")
print(f" Simulations: {len(job['sim_ids'])}")
print(f" Created: {job['created_at']}")Both tiers
Retrieving results
Use get_sim_data(sim_id) to download output files as Polars DataFrames. This works for both cached sim_ids (free tier) and sim_ids from your own jobs (pro tier). See Download data for the full list of output files and bulk download, and Output format for the L2 and Ticks DataFrame schemas.
Get job results summary (Pro)
job_id = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=5,
)["job_id"]
results = client.simulation.get_job_results(job_id)
print(f"Completed: {results['completed']}/{results['total_simulations']}")
for sim in results["simulations"]:
if sim["status"] == "completed":
print(f"Sim {sim['sim_id']}")
print(f" Files: {sim['available_files']}")
print(f" Metrics: {sim['metrics']}")Understanding simulation IDs
Each Monte Carlo run has a unique sim_id with the format:
{provider}:{exchange}:{symbol}:{cal_date}:{gen_method}:{cal_hash}:{time}:{sim_hash}:{scenario}_{hash}:{algo}:{run}For example: omd:hkex_securities:9999.HK:2025-09-02:ABM_v1.37.0:d96cf520:0930-1600:c8961b94:flash_crash_6e6d5b3a:baseline:0000
The sim_id encodes all configuration parameters (including gen_method, the engine version that produced the run), making it deterministic and reproducible. The final :{run} field is the zero-padded Monte Carlo run index — take an example_sim_id and swap this last field to address each run (…:0000, …:0001, …). Don't hand-build sim_ids field-by-field; always start from an example_sim_idreturned by list_cached() or run().