← All writing

Introducing Pulse: Agent-Based Market Simulation via API

In this blog, we will introduce our agent-based market simulator API. This is a highly sophisticated market simulation tool used by a number of financial firms. We are proud to open this up to the public through a number of recent partnerships. We will soon be also giving access to our foundation model for market simulation, to stay in the loop sign up to our waitlist here.

Markets are complex. Their dynamics emerge from the interactions of thousands of traders all trying to beat each other to make the most profit. Pulse is our market simulation product. We use two different approaches to generate plausibly realistic orderbook level data; an agent-based model (the glass box approach) and a foundation model (the black box approach). Here we are just introducing the agent-based model.

The agent-based model is more intuitive. An asset’s price emerges from dynamic interactions of market participants, this is exactly how the ABM works. We have 5 classes of agent, all competing to make the most profit on a realistically reactive price path. In our model we use the following agents:

  • Deep Liquidity providers - Provides liquidity deep in the book, dampening price impact.
  • Market Makers - Adjust quotes and inventory to capture the spread and stabilise trading.
  • Fundamental Traders - Trades on mispricing, anchoring long term price direction.
  • Momentum traders - Trade with trends, amplifying short term price movements.
  • Noise Traders - Add random, sentiment driven flow.

All these agents are interacting with a central exchange which is an exact replica of the Hong Kong exchange. The result is realistic price dynamics validated using stylised facts, LOB bench metrics and a suite of other metrics derived by us. We encourage you to pull the data and evaluate it against your own realism metrics. To aid in this, the below steps will guide you through pulling data.

Connecting to the API

Getting started requires an API key and the simudyne Python package. Once installed, you instantiate a client and you’re ready to go. To install and generate an API key please see pulse.simudyne.com/docs.

from simudyne import PulseABM

client = PulseABM(api_key="YOUR_API_KEY")

Listing Cached Simulations

Pulse caches simulation runs by symbol and date. A single call returns all cached simulations for a given stock on a given day, organised by scenario:

cached = client.simulation.list_cached(symbol="700.HK", date="2025-09-01")

# Group by scenario, showing the run with the most paths
by_scenario = {}
for s in cached["simulations"]:
    sc = s["scenario"]
    if sc not in by_scenario or s["n_runs"] > by_scenario[sc]["n_runs"]:
        by_scenario[sc] = s

Each entry in the response contains metadata: the scenario label ("normal", "flash_crash", etc.), the number of independent simulation runs, and an example simulation ID that encodes the base path for fetching individual run data, to load multiple sims, the number at the end of the sim_id will need iterating.

Fetching Simulation Data

Individual run data is fetched as a Parquet file and returned as a pandas DataFrame. Each row is a timestep; the columns include time and mid_price. Looping over all runs and concatenating gives the full ensemble:

frames = []

for sim in by_scenario.values():
    scenario = sim["scenario"]
    if scenario not in LABELS:
        continue
    base = sim["example_sim_id"].rsplit(":", 1)[0]
    for i in range(sim["n_runs"]):
        try:
            run_df = client.simulation.get_sim_data(f"{base}:{i:04d}", "mid_price_by_min.parquet")
        except Exception:
            continue
        run_df = pd.DataFrame({
            "time":      run_df["time"].to_list(),
            "mid_price": run_df["mid_price"].to_list(),
            "scenario":  scenario,
            "run":       i,
        })
        frames.append(run_df)

all_runs = pd.concat(frames, ignore_index=True)

Visualising Monte Carlo Paths

With the full ensemble in all_runs, plotting is straightforward with matplotlib:

fig, ax = plt.subplots(figsize=(14, 5))

for scenario, group in all_runs.groupby("scenario"):
    color = COLORS[scenario]
    label = LABELS[scenario]
    first = True
    for _, run in group.groupby("run"):
        ax.plot(run["time"].to_list(), run["mid_price"].to_list(),
                color=color, linewidth=0.6, alpha=0.35,
                label=label if first else "_nolegend_")
        first = False

ax.set_title("700.HK  2025-09-01 — Monte Carlo paths (mid price by minute)")
ax.set_xlabel("Time")
ax.set_ylabel("Mid Price")
ax.legend(title="Scenario")
fig.autofmt_xdate()
plt.tight_layout()
plt.savefig("mc_paths_700HK_20250901.png", dpi=150)
plt.show()

The interactive version of the same chart is shown below. Hover to inspect individual paths, click legend entries to toggle scenarios:

Each thin line is one independent Monte Carlo run. The black paths follow a normal trading day; the red paths undergo a flash crash at 10:30.

What’s Next

This walkthrough covered the basics: connecting to the API, fetching a Monte Carlo ensemble, and visualising scenario paths. The flash crash scenario is just one example, Pulse supports a range of parameterisable market conditions (can be viewed using client.simulation.list_scenarios()), and the same pipeline works for any supported symbol and date.

From here you might want to:

  • Examine stylised facts (autocorrelation, fat tails, volatility clustering) on the raw tick data
  • Benchmark the LOB against real market microstructure metrics
  • Explore other scenarios

In an upcoming post we’ll dig into our validation proceedure. After that we’ll introduce our foundation model approach, a black-box alternative trained on real market data that complements the ABM for applications where interpretability matters less than distributional realism. To get early access, sign up to the waitlist at pulse.simudyne.com.