Simulations
Run agent-based market simulations and retrieve results.
HTTP endpoints on this page
- GET/simulation/cached
- POST/simulation/run
- GET/simulation/jobs
- GET/simulation/jobs/{job_id}/status
- GET/simulation/jobs/{job_id}/results
- GET/simulation/jobs/{job_id}/logs
- GET/simulation/results/{sim_id}/data/{filename}
Parameters and response shapes for each are in the API reference.
A simulation replays one calibrated trading day of one instrument through the agent-based model (ABM), as several independent Monte Carlo runs. Use it to see how the book behaves under a market event, or how an execution algorithm would have fared against it. Everything lives 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()
"Pro" means a pro or demo key. Foundation-model runs use the same run() with engine="fm" — see Foundation models.
Methods — full reference in the API reference
simulation.list_cachedBrowse the pre-run baseline simulations.Any tiersimulation.runSubmit a Monte Carlo simulation job.Prosimulation.get_job_statusTrack a job through to completion.Prosimulation.get_jobsList the jobs you have submitted.Prosimulation.get_job_resultsParams, metrics and file listings for a whole job.Prosimulation.get_job_logsRead the engine log when a run fails.Prosimulation.get_sim_dataDownload one output file as a DataFrame.Any tiersimulation.list_scenariosScenario names and descriptions (local).Any tiersimulation.get_scenario_defaultsDefault parameters for one scenario (local).Any tier
Cached simulations (free tier)
A fixed library 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. Only baselines are cached — nothing with an execution algorithm attached. Each group carries an example_sim_id you can pass straight to get_sim_data().
cached = client.simulation.list_cached(symbol="700.HK")
print(f"Found {cached['total']} simulation groups")
sim = cached["simulations"][0]
print(sim["symbol"], sim["date"], sim["scenario"], sim["n_runs"], "runs")
df = client.simulation.get_sim_data(sim["example_sim_id"])
print(df.shape)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. Your own job runs are never charged, and pro and demo tiers are unlimited. A request that would exceed the allowance returns HTTP 429 without charging anything. Check where you stand with client.profile.downloads(). If you need more than the default, email support@simudyne.com — we can raise your daily limit without a Pro upgrade.
Submitting a simulation (Pro)
An instrument is a compound key of four fields — symbol, cal_date, provider and exchange — and all four are required. A combination absent from calibrated_data() is rejected rather than calibrated on demand.
run() returns as soon as the job is queued, with a job_id for the whole submission and one sim_id per run. n_runs defaults to 5 on the ABM. The master seed (default 42) derives every per-run seed, so the same seed, instrument and date always reproduce the same paths. Each field belongs to one engine: passing an FM field such as model_id to an ABM run (or scenario to an FM run) is a 422, not silently ignored.
result = client.simulation.run(
symbol="700.HK",
cal_date="2025-09-02",
provider="omd",
exchange="hkex_securities",
n_runs=2,
)
job_id = result["job_id"]
print(f"Submitted job {job_id} with {len(result['sim_ids'])} simulations")Market scenarios
Inject a market event on top of the calibrated background agents to stress-test strategies. Set scenario 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 |
Tune a scenario with scenario_params. There is no side: each scenario has its own direction, so pick the scenario for the direction. Any key you omit keeps its default; any other key, or an out-of-range value, is a 422 — even with scenario="normal", where the params otherwise have no effect.
| Key | Meaning |
|---|---|
| impact_multiplier | Total scenario volume as a multiple of average resting liquidity (bid + ask). > 0. |
| order_size_ratio | Each child order as a fraction of average resting liquidity. In (0, 1]. |
| order_freq | Spacing between child orders: number + ms, s, min (or m) or h, e.g. "500ms", "1min". |
| start_time | Wall-clock time to begin injection, "HH:MM" or "HH:MM:SS". |
Defaults, which mirror the engine's scenario builders. get_scenario_defaults(scenario) reads them from the installed SDK, so prefer it if the two ever disagree.
| Scenario | impact_multiplier | order_size_ratio | order_freq | start_time |
|---|---|---|---|---|
| flash_crash | 150.0 | 0.02 | 100ms | 10:30:00 |
| buy_panic | 150.0 | 0.02 | 100ms | 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
Pass an exec_algos list to run() to trade a strategy against the simulated book. Several configs compare strategies against one shared baseline; sim_ids then come back as baseline[0:n_runs], then strategy_0[n_runs:2*n_runs], and so on.
| Type | What it does | Required keys |
|---|---|---|
| twap | Time-weighted: a constant rate across the window | order_size, horizon |
| vwap | Volume-weighted: same config as TWAP, schedule follows the calibrated volume profile | order_size, horizon |
| css | Custom static schedule you supply as a pd.Series indexed by full datetimes | orders |
| Key | Meaning |
|---|---|
| order_size | Total size in LOTS, not shares (× lot_size from calibrated_data() for shares: 50 lots of 700.HK = 5,000 shares). Positive sells, negative buys, unless side is given. |
| horizon | Execution window in seconds, e.g. 7200 for two hours. |
| orders | css only. Sizes indexed by datetime; orders outside HKEX continuous hours (09:30–12:00, 13:00–16:00) are dropped. |
| start_time | "HH:MM:SS" to begin. Defaults to market open. |
| frequency | Seconds between child orders (default 1). |
| side | "buy" or "sell". Inferred from the sign of order_size (or of the orders values) when omitted. |
| random_offset | twap/vwap: bool, randomise each order within its interval (default True). css: float seconds of jitter (default None, off). |
Execution runs write extra output files (exec_schedule, schedule_by_min, exec_results) — see Download data.
How a job progresses
Each simulation in a job moves through queued → running → complete, or fails. get_job_status() reports every run plus two server-derived flags: is_complete (every run finished) and has_errors (any run failed). Prefer those flags to reading statuses yourself: success is reported as complete (older jobs: completed) and failure as failed or error, so test membership, e.g. status in ("complete", "completed").
A failed run carries its own error_message; for more, get_job_logs() returns the worker's engine log (written once the job reaches a worker). get_jobs() recovers job IDs from earlier sessions, and get_job_results() returns params, metrics and the available files for every run without downloading any data.
# the most recent job of yours
job_id = client.simulation.get_jobs(limit=1)["jobs"][0]["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: 2
Status: {'complete': 1, 'running': 1}
Complete: FalseUnderstanding 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().