Simulations
Run agent-based market simulations and retrieve results.
HTTP endpoints on this page
- GET/simulation/cached
- GET/simulation/sample
- POST/simulation/run
- POST/simulation/lrm/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.
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()
Methods on this page
- list_cachedBrowse the pre-run baseline simulations.Any tier
- runSubmit a Monte Carlo simulation job.Pro
- get_job_statusTrack a job through to completion.Pro
- get_job_logsRead the engine log when a run fails.Pro
- get_jobsList the jobs you have submitted.Pro
- get_sim_dataDownload one output file as a DataFrame.Any tier
- get_job_resultsMetrics and file listings for a whole job.Pro
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.
simulation.list_cached(symbol=None, date=None, scenario=None)List the cached baseline simulations available to your account, grouped by symbol, date and scenario.Any tier
Only baseline simulations are cached — nothing with an execution algorithm attached. Every filter is optional, and filters combine with AND. Omitting all three returns the whole catalogue.
Parameters
- symbol : str, optional
- Restrict to one trading symbol, e.g.
"700.HK". - date : str, optional
- Restrict to one calibration date,
"YYYY-MM-DD". - scenario : str, optional
- Restrict to one scenario name, e.g.
"flash_crash". See Market scenarios for the full set.
Returns
dict
- total : int
- Number of distinct symbol/date/scenario groups matched.
- simulations : list of dict
- One entry per group, carrying
example_sim_id,symbol,date,scenario,n_runs,cal_hash,sim_hashandtime_range. Feedexample_sim_idstraight toget_sim_data(); never assemble one by hand.
See also
- get_sim_data — Download the parquet output of one run in a group.
- run — Submit a simulation of your own, on any catalogued instrument (Pro).
Examples
# 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 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(), which documents every field it returns. 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, email support@simudyne.com — we can raise your daily limit without a Pro upgrade.
Pro tier
Submit a simulation
simulation.run(symbol="", cal_date="", provider="", exchange="", engine="abm", n_runs=None, seed=42, scenario=None, scenario_params=None, exec_algos=None, model_id="", duration_minutes=None, horizon=None, device="", model_args=None)Submit a Monte Carlo simulation job. Returns as soon as the job is queued — it does not wait for results.Pro
An instrument is a compound key of four fields, and all four are required. A combination absent from calibrated_data() is rejected rather than calibrated on demand.
run() drives both engines. This page covers the default, engine="abm" (the agent-based model). Set engine="fm" to run a foundation model instead, which takes model_id, duration_minutes, horizon, device and model_args. Those are documented on the Foundation models page. Each field belongs to one engine. If you pass a field to the other engine, the SDK still sends it, and the API rejects the request with a 422 rather than silently ignoring it. For example, scenario on an FM run, or model_id on an ABM run, raises PulseAPIError.
Parameters
- symbol : str
- Trading symbol, e.g.
"700.HK"or"0005.HK". - cal_date : str
- Calibration date,
"YYYY-MM-DD". Must already be calibrated; every date returned bycalibrated_data()qualifies, and the endpoint filters server-side, so there is no status or stage for you to check first. - provider : str
- Data provider the symbol is sourced from, e.g.
"omd"or"bmll". - exchange : str
- Exchange protocol name, e.g.
"hkex_securities"or"hkex_derivatives". - n_runs : int, default None
- Independent Monte Carlo runs. Each produces its own
sim_id; together they form the one group that the free-tier download quota counts. Left asNone, the server picks the default for the engine:5forabmand1forfm. FM jobs are capped at8runs. - seed : int, default 42
- Master random seed. Per-run seeds derive from it, so a job is reproducible as a whole and run-by-run. The same seed, symbol and date always produce the same Monte Carlo paths — send the seed alongside the job config and a colleague can regenerate your exact runs.
- scenario : {"normal", "flash_crash", "buy_panic", "gradual_selloff", "trending_up", "trending_down"}, default "normal"
- Market event injected on top of the calibrated background agents.
"normal"injects nothing. See Market scenarios. - scenario_params : dict, optional
- Overrides for the chosen scenario's defaults — keys below. They have no effect when
scenario="normal", but they are still validated, so an unknown key or an out-of-range value is a422even then. - exec_algos : list of dict, optional
- Execution algorithms to trade against the simulated book. Each dict requires a
typekey of"twap","vwap"or"css"; remaining keys are documented under Execution algorithms. Passing several configs compares strategies against one shared baseline.
Returns
dict
- job_id : str
- Handle for the whole submission. Poll it with
get_job_status(). - sim_ids : list of str
- One
sim_idper queued run, in run order. With execution algorithms the ordering isbaseline[0:n_runs], thenstrategy_0[n_runs:2*n_runs], and so on. Older SDK releases returned this asqueued_sim_ids; readsim_ids. - n_runs : int
- Runs per strategy, after the server has applied its default.
- engine : {"abm", "fm"}
- The engine the job was submitted to.
Raises
- PulseAPIError
403on a free-tier key —run()is Pro and above, and anis_adminflag is not a bypass.422when the four instrument fields do not name a calibrated instrument (the message lists the dates that are calibrated), whenscenario_paramsfails validation, or when a field belongs to the other engine. See Errors.
See also
- get_job_status — Poll a submitted job through to completion.
- get_job_results — Per-simulation metrics and file listings once it is done.
- list_cached — Pre-run baselines, free tier, no job to submit.
Examples
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["sim_ids"]
print(f"Submitted job {job_id} with {len(sim_ids)} simulations")A flash crash mid-session, and a TWAP working 50 lots across two hours — the scenario and algorithm keys are documented above:
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", # trigger mid-session
"impact_multiplier": 20.0, # severe liquidity drain
"order_freq": "500ms", # high-frequency cascade
},
exec_algos=[{
"type": "twap",
"order_size": 50, # lots, not shares
"horizon": 7200, # two hours
"start_time": "09:30:00",
"frequency": 30,
}],
)
job_id = result["job_id"]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 |
scenario_params keys
- impact_multiplier : float, optional
- Total scenario volume as a multiple of average total resting liquidity, bid and ask combined. Must be greater than 0.
- order_size_ratio : float, optional
- Size of each child order as a fraction of average resting liquidity. Must be in
(0, 1]. - order_freq : str, optional
- Spacing between child orders, as a number followed by a unit:
ms,s,min(orm) orh. For example"500ms","1s"or"1min". - start_time : str, optional
- Wall-clock time to begin injection,
"HH:MM"or"HH:MM:SS", e.g."10:30:00".
There is no side: each scenario has its own direction — flash_crash, gradual_selloff and trending_down sell; buy_panic and trending_up buy. Pick the scenario for the direction.
Any other key is rejected with a 422 that names the allowed keys. These checks run whatever the scenario, including "normal".
Every key can be overridden for any non-normal scenario, and any you omit keep the defaults below. These mirror the engine's own scenario builders — get_scenario_defaults(scenario) reads them from the installed SDK, so prefer it over this table 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
Test execution strategies by passing an exec_algos list to run(). Each config is a dict; type decides which of the remaining keys apply.
Algorithm config keys
- type : {"twap", "vwap", "css"}
- Algorithm to run. Required, and the only key common to all three.
- order_size : int
- Total size to execute, in lots — the unit the engine and the calibrated background agents both work in, not shares. Multiply by the instrument's
lot_sizefromcalibrated_data()to reason in shares: 50 lots of 700.HK (lot_size 100) is 5,000 shares. The sign sets direction unlesssideis given — positive is sell, negative is buy. Required fortwapandvwap. - horizon : int
- Execution window in seconds, e.g.
7200for two hours. Required fortwapandvwap. - orders : pandas.Series
- Explicit schedule: sizes indexed by a full datetime index, date and time combined. Required for
css, and unused by the other two. Orders falling outside HKEX continuous trading hours (09:30–12:00, 13:00–16:00) are silently dropped. - start_time : str, optional
- When to begin,
"HH:MM:SS". Defaults to market open. - frequency : int, default 1
- Seconds between child orders.
- side : {"buy", "sell"}, optional
- Direction. Inferred when omitted — from the sign of
order_sizefortwap/vwap, from the sign of theordersvalues forcss. - random_offset : bool for twap/vwap, float (seconds) for css, optional
- Jitter on order timing, and the one key whose type depends on
type. Fortwap/vwapit is a flag that randomises each order within its frequency interval, defaultTrue. Forcssit is a window in seconds — each order is offset by a random amount up to that many seconds — defaultNone, meaning disabled.
TWAP (Time-Weighted Average Price)
{"type": "twap", "order_size": …, "horizon": …} — a constant rate across the window. Every key it takes is in Algorithm config keys above, and run()'s second example shows one in place.
VWAP (Volume-Weighted Average Price)
{"type": "vwap", "order_size": …, "horizon": …} — identical configuration to TWAP, but the schedule follows the calibrated volume profile rather than the clock.
CSS (Custom Static Schedule)
{"type": "css", "orders": …} — you supply the schedule yourself as a pd.Series indexed by a full datetime, instead of a size and a horizon. The cookbook has a schedule builder and a recipe for comparing several schedules against one shared baseline.
Tracking a job
simulation.get_job_status(job_id)Return the status of every simulation in a job. Each one moves through queued → running → complete, or fails.Pro
Parameters
- job_id : str
- A job handle from
run()orget_jobs().
Returns
dict
- job_id : str
- The job identifier, echoed back.
- engine : {"abm", "fm"}
- Which engine ran the job. For an
fmjob the response also carriesstatus,messageanddetailfrom the GPU scheduler, which is where a state such as waiting for a GPU shows up. See Foundation models. - total_simulations : int
- Number of simulations in the job.
- status_summary : dict
- Count by status, e.g.
{'complete': 3, 'running': 2, 'failed': 0}. - is_complete : bool
- True once every simulation has finished. Derived server-side — prefer it to reading status_summary yourself.
- has_errors : bool
- True if any simulation failed. Pair it with
get_job_logs()to find out why. - simulations : list of dict
- Per-simulation detail:
sim_id,status,error_message,symbol_idandtimestamp. A failed run carries its ownerror_messagehere, which is usually enough on its own.
Notes
Two spellings of success reach clients — complete and completed — and two of failure, failed and error. The engine writes complete. Test for membership rather than equality: status in ("complete", "completed"). Better still, read is_complete and has_errors, which the API derives for you and which this ambiguity cannot reach.
See also
- get_job_logs — The engine log for a job, once has_errors is true.
- get_job_results — Metrics and file listings for the finished runs.
Examples
# setup: 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: 5
Status: {'complete': 3, 'running': 2, 'failed': 0}
Complete: Falsesimulation.get_job_logs(job_id)Return the engine run log for one of your jobs, as plain text.Pro
has_errors tells you a simulation failed; this tells you why. The worker writes a diagnostic log per job — it is the thing to read when a run fails or finishes with nothing plottable, and the thing to attach if you send it to us.
Parameters
- job_id : str
- A job handle from
run()orget_jobs().
Returns
str
The log text, whole.
Raises
- PulseAPIError
404when the job does not exist, is not yours, or wrote no log. The log is written once a job reaches a worker, so a job rejected before that point has none. See Errors.
Examples
from simudyne import PulseAPIError
# setup: the most recent job of yours
job_id = client.simulation.get_jobs(limit=1)["jobs"][0]["job_id"]
try:
print(client.simulation.get_job_logs(job_id)[:2000])
except PulseAPIError as exc:
print(f"no log for {job_id}: {exc}")simulation.get_jobs(limit=100)List the jobs you have submitted, newest first. The way to recover job IDs from a previous session.Pro
Parameters
- limit : int, default 100
- Maximum jobs to return. Capped at 500.
Returns
dict
- jobs : list of dict
- One entry per job, with
job_id,sim_idsandcreated_at. - total : int
- Jobs you have submitted in total; more than len(jobs) means the limit cut the list.
- limit : int
- The limit the page was read with.
- offset : int
- Jobs skipped before this page — always 0 from the SDK.
Examples
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
simulation.get_sim_data(sim_id, filename="sim_data.parquet")Download one output file of one simulation run as a Polars DataFrame.Any tier
Works for both cached sim_ids (free tier) and sim_ids from your own jobs (pro tier).
Parameters
- sim_id : str
- The run to fetch, from
list_cached()orrun(). - filename : {"sim_data.parquet", "mid_price_by_min.parquet", "l2_by_second.parquet", "exec_schedule.parquet", "schedule_by_min.parquet", "exec_results.parquet"}, default "sim_data.parquet"
- Which output file to download. The default is the full output — order book and orders. The three
exec_*andschedule_*files exist only for runs that had an execution algorithm attached. Column schemas are in Output format.
Returns
polars.DataFrame
The file, read into memory.
Notes
On the free tier, the first download from a group you have not downloaded before consumes one unit of the daily allowance and returns HTTP 429 once exhausted. Groups you already own stay free forever, on any run or file format, and your own job runs are never charged. See Download limits above.
See also
- get_bulk_data — Several runs or files at once, as one ZIP.
- Output format — Book and event column schemas for each file.
Examples
# setup: any cached simulation
sim_id = client.simulation.list_cached()["simulations"][0]["example_sim_id"]
df = client.simulation.get_sim_data(sim_id)
print(df.shape)
print(df.head())simulation.get_job_results(job_id)Return params, metrics and available files for every simulation in a job, without downloading any of the data.Pro
Parameters
- job_id : str
- A job handle from
run()orget_jobs().
Returns
dict
- job_id : str
- The job identifier, echoed back.
- total_simulations : int
- Simulations in the job.
- completed : int
- How many have finished.
- simulations : list of dict
- Per-simulation results:
sim_id,status,available_files,paramsandmetrics. Pass asim_idand one of itsavailable_filestoget_sim_data()to pull the data itself.
Examples
# setup: the most recent job of yours
job_id = client.simulation.get_jobs(limit=1)["jobs"][0]["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"] in ("complete", "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().