Scenarios (deterministic replay)
A scenario is a saved description of a single trade: symbol, side, quantity, limit price, strategy, plus a numeric seed for the market simulator's PRNG. Running the same scenario twice should produce the same fills, the same slippage, and the same child-order count. If it doesn't, the algo behaviour has changed, which is something a regression test should catch automatically.
This harness covers two things at once:
- Algo correctness across releases. Save a baseline run today, re-run it after every code change. Drift in the result indicates the change altered execution behaviour.
- The shape of MiFID II RTS 6 conformance testing. Real firms are required to formally validate algorithmic trading systems and demonstrate they behave as documented. We don't have to file paperwork for a simulator, but the data structures used to record runs match what a regulated firm would reach for.
How it works
Section titled “How it works”- Determinism comes from the simulator.
market-sim/rng.tsreplacesMath.random()with a seeded mulberry32 PRNG whenMARKET_SIM_SEEDis set orPOST /seedis called at runtime. Same seed in, same price path out. Without the seed, the simulator usesMath.random()and behaves as before. - Each saved scenario carries its seed. The orchestrator
scenarios/orchestrator.tscallsPOST /seedto reset the simulator's PRNG before submitting the order, so every run starts from the same regime and price baseline. - The orchestrator submits the order through the normal pipeline. Risk engine, OMS, algos, EMS, and journal: the same path as a manually-placed order. Nothing about the order flow changes for scenario-driven submissions.
- The journal is the source of truth. The orchestrator polls the journal for the parent order until terminal state (or 30s timeout), then computes an actual outcome (fill count, total filled, weighted-average price, slippage in basis points) and writes a run row.
- Diff against expectations. If the saved scenario specifies expected outcomes with tolerances, the run is marked
completed(within tolerance),mismatched(outside tolerance), orfailed(no order in journal, submit failed, etc.).
Schema
Section titled “Schema”Two tables in the scenarios schema (migration 0015_scenarios.sql):
scenarios.scenarios — one row per saved scenario. Owned by a user via users.users(id) cascade; unique on (user_id, name).
Columns: id, user_id, name, description, spec (JSONB), expected (JSONB), timestamps.
scenarios.runs — one row per execution. Append-only.
Columns: id, scenario_id, user_id, triggered_at, completed_at, parent_order_id, actual (JSONB), diff (JSONB), status, error.
The parent_order_id column lets you drill from a mismatched run all the way through journal.events to the OMS validation, the risk-engine decision, and every child order the EMS produced.
All routes proxy via the gateway with a session cookie. Trader and admin roles can write; any authenticated user can read their own scenarios.
| Method | Path | Body / params | Returns |
| --- | --- | --- | --- |
| GET | /api/gateway/scenarios | | { scenarios: Scenario[] } |
| POST | /api/gateway/scenarios | { name, description?, spec, expected? } | { scenario: Scenario } |
| GET | /api/gateway/scenarios/:id | | { scenario: Scenario } |
| PUT | /api/gateway/scenarios/:id | partial fields | { scenario: Scenario } |
| DELETE | /api/gateway/scenarios/:id | | 204 |
| POST | /api/gateway/scenarios/:id/run | | { run: ScenarioRun } |
| GET | /api/gateway/scenarios/:id/runs?limit=20 | | { runs: ScenarioRun[] } |
spec shape: { seed, symbol, side, quantity, limitPrice, strategy, algoParams?, durationMs? }. expected shape (all optional): { fillCount?, totalFilled?, avgFillPriceBps?, slippageBps?, tolerance? }.
In-app panel
Section titled “In-app panel”Add the Scenarios panel to any workspace via + Add Panel. It shows your saved scenarios with one-click run, plus the run history for the selected scenario with status pill, fill ratio and slippage in basis points. The panel is gated to trader and admin roles.

A run is synchronous from the panel's perspective. Clicking Run waits for the orchestrator to finish (or hit its 30s timeout) before the row appears in history. Run history polls every 5s for any newly-appended rows.
Pinned to risk config
Section titled “Pinned to risk config”Each /check response from the Risk Engine includes riskConfigVersion: <id> from the immutable risk.config_versions table (migration 0014_risk_config_versions.sql). Combined with the journal's parent order ID and the saved scenario seed, every past run can be reconstructed exactly: same prices, same risk thresholds, same algo parameters. That is the property regulators ask about: "why did we reject Alice's order at 3pm last Tuesday" has a deterministic answer.