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

  1. List cached simulations with list_cached()
  2. Download data with get_sim_data(sim_id)

Pro tier

  1. Submit a job with run() — returns a job_id
  2. Track progress with get_job_status(job_id)
  3. Find past jobs with get_jobs()
  4. 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.

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_hash and time_range. Feed example_sim_id straight to get_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

python
# 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 by calibrated_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 as None, the server picks the default for the engine: 5 for abm and 1 for fm. FM jobs are capped at 8 runs.
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 a 422 even then.
exec_algos : list of dict, optional
Execution algorithms to trade against the simulated book. Each dict requires a type key 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_id per queued run, in run order. With execution algorithms the ordering is baseline[0:n_runs], then strategy_0[n_runs:2*n_runs], and so on. Older SDK releases returned this as queued_sim_ids; read sim_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
403 on a free-tier key — run() is Pro and above, and an is_admin flag is not a bypass. 422 when the four instrument fields do not name a calibrated instrument (the message lists the dates that are calibrated), when scenario_params fails 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

python
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:

python
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:

ScenarioDescription
normalNo injection — background agents only (default)
flash_crashLarge rapid SELL depleting bid-side liquidity
buy_panicLarge rapid BUY depleting ask-side liquidity
gradual_selloffSlow sustained SELL over extended period
trending_upSmall steady BUY producing persistent uptrend
trending_downSmall 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 (or m) or h. 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.

Scenarioimpact_multiplierorder_size_ratioorder_freqstart_time
flash_crash150.00.02100ms10:30:00
buy_panic150.00.02100ms10:30:00
gradual_selloff10.00.055s10:30:00
trending_up5.00.0330s10:30:00
trending_down5.00.0330s10: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_size from calibrated_data() to reason in shares: 50 lots of 700.HK (lot_size 100) is 5,000 shares. The sign sets direction unless side is given — positive is sell, negative is buy. Required for twap and vwap.
horizon : int
Execution window in seconds, e.g. 7200 for two hours. Required for twap and vwap.
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_size for twap/vwap, from the sign of the orders values for css.
random_offset : bool for twap/vwap, float (seconds) for css, optional
Jitter on order timing, and the one key whose type depends on type. For twap/vwap it is a flag that randomises each order within its frequency interval, default True. For css it is a window in seconds — each order is offset by a random amount up to that many seconds — default None, 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() or get_jobs().

Returns

dict

job_id : str
The job identifier, echoed back.
engine : {"abm", "fm"}
Which engine ran the job. For an fm job the response also carries status, message and detail from 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_id and timestamp. A failed run carries its own error_message here, 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

python
# 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']}")
Output
Total: 5
Status: {'complete': 3, 'running': 2, 'failed': 0}
Complete: False
simulation.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() or get_jobs().

Returns

str

The log text, whole.

Raises

PulseAPIError
404 when 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

python
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_ids and created_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

python
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() or run().
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_* and schedule_* 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

Examples

python
# 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() or get_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, params and metrics. Pass a sim_id and one of its available_files to get_sim_data() to pull the data itself.

Examples

python
# 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:

text
{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().