Scenario contract¶
A replay scenario uses either one strict JSON object or a strict JSON Lines stream. Exact prices, weights, quantities, money, and sequences are canonical JSON strings. Counts and basis points are JSON integers. Unknown, missing, duplicate, noncanonical, and non-finite values fail parsing.
Use the v16 demo as the canonical complete example. The scenario JSON Schema provides structural validation. The engine parser also enforces cross-field and cross-record invariants. Diagnostics identify the failed field or array item. Stream diagnostics additionally retain the record line and sequence.
trading-engine --input scenario.json --validate-only
trading-engine --input scenario.jsonl --input-format jsonl --validate-only
Use --input - --input-format jsonl to read a stream from standard input. The CLI spools at most
1 GiB to a private temporary file so the same bytes can be hashed, validated, replayed, and hashed
again. Batch JSON cannot use standard input.
JSON Lines stream¶
Use the stream for histories that should not be materialized inside the engine. The first record
is scenario_header and carries the static top-level fields. Each market_slice record carries
one complete slice and the intents evaluated after that slice. The final scenario_end record
declares the number of slices. It is required even for an empty stream, so a truncated valid
prefix cannot be mistaken for a complete scenario.
Every record has exactly contract_version, scenario_sequence, record_type, and payload.
The contract version is repeated, and scenario_sequence is contiguous from one. Intents are
adjacent to their decision slice rather than stored in a future-looking global schedule. Before
replay, the reader checks each intent-bearing slice against the next slice's start time while
retaining only those two records.
The batch object and stream header share one domain-construction path and the same static semantic checks. Stream items reuse the batch slice and intent validators directly; no synthetic batch scenario is constructed.
The stream record JSON Schema validates each line, and the v16 stream fixture is the canonical example. The engine validates the entire stream before creating a journal. It then replays one record at a time without retaining prior slices, scheduled batches, or audit events. Reducer state still retains current account, order, target, and latest-bar state required by execution semantics.
Top-level fields¶
| Field | Meaning |
|---|---|
contract_version |
Required string identifying this file contract; v16 is "16" |
metadata |
Required arbitrary JSON object preserved for provenance and ignored by execution |
run_id |
Stable identity used in generated IDs |
base_currency |
Reporting currency used for aggregate risk and valuation |
initial_portfolio |
Signed cash and positions with accounting history, marks, and FX state |
instruments |
Approved executable-instrument catalog, at most 4,096 entries |
venue_calendars |
Immutable venue/session policies covering every configured instrument |
risk |
Signed position, exposure, leverage, margin, and borrow policy |
execution |
Capacity and fee configuration |
financing |
Borrow/cash day-count, compounding, locate, recall, and missing-data policies |
settlement |
Business-date calendars, per-instrument lags, and cash/position availability policies |
max_internal_events |
Positive reducer feedback cap, at most 100,000 |
schedule |
Intents emitted after named slices, at most 4,096 per batch |
slices |
Complete synchronized market observations |
Metadata may contain nested JSON values. Duplicate object keys and non-finite numbers are rejected
at any depth. Metadata is retained on Scenario.t but never affects execution.
An external strategy replay requires schedule: []. The JSON Lines form likewise requires every
slice record's intents array to be empty. This keeps one authoritative decision source: either
the scenario contract or the separate strategy protocol, never both.
Each JSON Lines record is limited to 1 MiB, excluding its line feed. The reader accepts a final record without a line feed and drains an oversized record without retaining bytes above the limit.
Use --journal - to write a journal to standard output. The engine first creates and verifies a
complete temporary journal, then copies only journal bytes to the pipe. The final run_completed
record and a zero process exit status signal completeness. Success summaries move to standard error,
and diagnostics always use standard error, so protocol, journal, and summary bytes never share one
stream. Pipes do not provide exclusive no-replace publication, atomic linking, retained partial
artifacts, directory synchronization, or restart durability. They cannot be combined with
--durable-artifacts.
Instruments, risk, and execution¶
Each instrument contains instrument_id, symbol, quote_currency, tick_size, and lot_size.
Identifiers and labels are nonempty and contain no whitespace or control characters. Tick and lot
sizes are positive exact values with at most six decimal places. Quote currencies may differ from
base_currency; initial_portfolio.cash contains every distinct quote currency plus the base
currency exactly once.
Initial portfolio¶
The v6 initial_portfolio contains cash, positions, marks, and fx_rates. Cash is signed and
has exact scenario-currency coverage. Each nonzero signed position names a catalog instrument and
records signed quantity and cost_basis, signed realized_pnl and dividend_pnl, and
nonnegative execution_fees and borrow_fees. Basis has the same sign as quantity. These P&L and
fee values are point-in-time histories; importing them does not apply them to cash again.
Position quantities align to instrument lots and respect long and short limits. Marks cover the position set exactly, are positive, and align to instrument ticks. FX rates cover every scenario currency exactly and the base rate is one. Before replay, the engine constructs the account, reconciles its valuation, and enforces gross exposure, leverage, and initial margin. It accepts negative cash when the complete marked account remains valid under the configured risk policy.
Venue calendars¶
Contract v7 requires every instrument to belong to exactly one explicit venue calendar. A calendar
is identified by venue_id, calendar_id, and calendar_version; version 1 is the only supported
calendar payload. Its sessions are unique and ordered by session_date, and each date declares
one policy: regular, early_close, or holiday. Holidays have no phases. Open sessions must
contain a regular phase and may also contain premarket, opening_auction, closing_auction,
and postmarket phases in market order. Phase intervals cannot overlap.
All phase boundaries are absolute RFC 3339 timestamps. Scenario producers, not the reducer, resolve venue-local civil times, time-zone database versions, daylight-saving changes, and clock effects. Calendar lookup rejects a date without an explicit policy; it never infers weekends, holidays, or hours from adjacent entries. This makes future DAY expiry, auction eligibility, settlement, and daily-bar publication policies depend on versioned input rather than ambient system state.
Risk contains portfolio-wide positive max_gross_exposure and max_leverage, annualized
short_borrow_bps, exactly one instrument_policies entry per catalog instrument, and an explicit
groups array. Instrument policies define order, long, short, notional, initial-margin,
maintenance-margin, and shorting limits. Groups carry versioned identities and explicit membership;
they may overlap and can constrain gross, long, short, absolute net, and gross-to-equity
concentration exposure. Admission and fill clipping include working-order reservations. Every
applicable group is enforced, with group identity providing deterministic tie ordering.
Contract v10 execution contains a stable model and a model-owned configuration. For
completed_bar_v1, configuration version "2" contains:
version, the strict model-configuration contract versionparticipation_bps, from 0 through 10,000fee_schedules, exactly one schedule per instrument. Each schedule has a stable ID, instrument, settlement currency, nullable minimum and maximum, and one or more named components.
Each component declares currency, kind (fixed, notional_bps, or per_unit), a signed
value, rounding (up, down, or nearest), and applies_to (any, maker, or taker).
Signed values permit rebates. Minimums and maximums are nonnegative and apply per fill after the
component values are converted into the settlement currency.
The engine advertises each model's scenario and configuration versions, required fields, supported
order types, data requirements, and limits through --capabilities.execution_model_contracts. The
v8 and earlier contracts retain completed-bar configuration version "1"; v3 and v4 preserve
their flat execution object unchanged.
Contract v13 introduced completed_bar_next_open_v1 and
completed_bar_adverse_touch_v1, each with strict configuration version "1". They retain
participation_bps and fee_schedules, and additionally require:
spread_model:fixed_half_spread_v1withhalf_spread_bpsfrom 0 through 10,000.impact_model:linear_participation_v1withcoefficient_bpsfrom 0 through 10,000 andmissing_volume_policyset torejectorzero_impact.
The next-open model does not infer intrabar limit fills. The adverse-touch model requires a one-tick trade-through. Both round price costs away from the reference to the instrument tick and journal reference, spread, impact, and final executable prices separately.
Schedule and intents¶
Schedule entries are positive, strictly increasing, and anchored to existing slices:
{
"after_slice_sequence": "1",
"intents": [
{
"type": "target_weights",
"targets": [
{ "instrument_id": "asset-a", "weight": "0.6" },
{ "instrument_id": "asset-b", "weight": "0.3" }
]
}
]
}
Supported intents are:
target_weightswith atargetsarray ofinstrument_idandweighttarget_quantitieswith atargetsarray ofinstrument_idandquantitysubmit_orderwith instrument, side, quantity, kind, and nullable limit pricecancel_orderwith a deterministicorder_idemit_metricwith a bounded stringnameand typedvalue. The value object declaresnumeric(a canonical decimal string),string, orboolean. Optionalunit,aggregation(last,sum,minimum,maximum, ormean), and up to 16 string dimensions carry reconciliation metadata. Dimension keys are unique and journal encoding sorts them lexicographically. Contracts through v15 retain the legacy string-only shape.
Both target forms contain every configured instrument exactly once. Weights and quantities are
signed. Gross absolute weight must not exceed max_leverage; quantity targets align to their
instrument lots and stay within the long and short position limits. A rebalance that crosses from
long to short, or short to long, first flattens the existing position and continues toward the
target on a later attempt.
A market submission uses "order_kind": "market" and "limit_price": null. A limit submission
uses "order_kind": "limit" and a canonical positive price. Static order size, lot, and tick
checks run during parsing; position and outstanding-order checks run in the reducer.
Market slices¶
Each slice has common timing, one bar per configured instrument, a complete set of currency-to-base FX marks, zero or more corporate actions, and effective-time borrow and cash-rate observations:
Timestamps use YYYY-MM-DD[Tt]HH:MM:SS, optional one-to-six fractional-second digits, and either
Z/z or a colonized numeric offset such as -05:00. Seconds range from 00 through 59.
Audit timestamps use the same boundary.
{
"slice_sequence": "1",
"start_at": "2026-01-02T14:30:00Z",
"end_at": "2026-01-02T21:00:00Z",
"available_at": "2026-01-02T21:00:01Z",
"received_at": "2026-01-02T21:00:02Z",
"bars": [
{
"instrument_id": "asset-a",
"open": "100",
"high": "105",
"low": "99",
"close": "104",
"volume": "100"
}
],
"fx_rates": [
{ "currency": "USD", "rate": "1" }
],
"corporate_actions": [],
"borrow_observations": [
{ "instrument_id": "asset-a", "effective_at": "2026-01-02T14:30:00Z", "available_quantity": "1000", "annual_rate_bps": 100, "recalled": false }
],
"cash_rate_observations": [
{ "currency": "USD", "effective_at": "2026-01-02T14:30:00Z", "credit_rate_bps": 100, "debit_rate_bps": 200 }
],
"settlement_failures": []
}
Use null volume when unavailable; it means unlimited simulation capacity, not zero. Sequences
are positive and strictly increasing. Slices do not overlap: each start is at or after the prior
end, so equal boundaries are valid. Receipt time never moves backward. Start precedes end,
availability does not precede end, and receipt does not precede availability. OHLC values satisfy
their usual range relationships. Volume may be fractional but must align to the instrument lot.
Each slice supplies exactly one positive FX rate for every scenario currency, and the
base-currency rate is exactly one.
Financing observations are unique per instrument or currency within a slice, effective no later
than the slice start, and strictly advance the effective time for their key across slices. A recall
has zero available quantity. The latest observation remains active until replaced. The top-level
financing object selects actual_365 or actual_360, simple or daily, reject or zero
missing-data handling, reject_order or clip_fill locate behavior, and
reject_new_shorts or close_out recall behavior.
The v12 settlement object selects total_cash or settled_cash buying power and
total_positions or settled_positions availability. Its immutable calendars contain ordered
canonical business dates, and each instrument has exactly one calendar and a lag from zero through
30 business days. A fill updates economic accounting immediately and creates a deterministic
instruction. Pending cash and quantity appear as unsettled attribution until the first slice on or
after the due date. A due instruction named in that slice's settlement_failures becomes failed
instead, retains its unsettled balances, and records the supplied reason.
Supported corporate actions are exact-ratio split, per-unit cash_dividend, stock_dividend,
rights, and spin_off records. Distribution records name a destination instrument, entitlement
ratio, basis allocation in basis points, and a fractional policy. reject fails on a non-lot
entitlement; cash_in_lieu requires an explicit destination-quote-currency price and journals the
delivered quantity, fractional quantity, allocated basis, fractional basis, and cash amount. Action
IDs are unique across the scenario. Actions are applied in canonical ID order before borrow fees
and matching. A split rescales the position, persistent target, and active orders while preserving
basis; it does not rescale unit-based risk limits. Split-adjusted positions and targets are
grandfathered under the existing reduce-only position policy. Split-adjusted orders remain active,
but each fill is bounded by max_order_quantity; GTC limit remainders may fill on later slices,
while market IOC remainders are cancelled. A dividend changes the quote-currency cash ledger and
realized dividend P&L, crediting a long and debiting a short.
Version 12 slices also carry lifecycle_events. Stable instrument_id never changes. An
identifier_change updates the current symbol and one named provider mapping with provenance;
halt and resume control whether new exposure is accepted. expiration and delisting are
terminal and require either hold or an explicit quote-currency cash_out price. Halts and
terminal events cancel active orders. Terminal events set persistent target exposure to zero and
cash-out clears the position with exact realized-P&L attribution.
Version 14 slices add market_events. A quote records bid/ask prices and displayed quantities; a
trade records price, quantity, and buy, sell, or unknown aggressor side. Every event also
records event_at, available_at, received_at, and a positive ingest_sequence. Events are
strictly ordered by availability, receipt, and ingest sequence; economic time cannot follow
availability, and no event may escape its containing slice's time or observability boundary.
Prices and quantities align to the instrument tick and lot. The
quote-trade fixture demonstrates passive
fills and has an equivalent bounded JSON Lines replay.
Version 15 slices add order_book_events. Every configured instrument supplies a fresh full
snapshot followed by contiguous absolute set, delete, and aggressor-classified trade updates.
Snapshots contain price-ordered unique bid and ask levels. Crossed states are invalid, while locked
books are accepted. Runtime validation enforces the configured max_depth_levels, known levels on
delete, sequence continuity, slice observability, and tick/lot alignment. Marketable orders walk
the visible book; passive limits queue behind displayed same-price depth, with reductions moving
them forward and additions joining behind. The
order-book fixture demonstrates bounded
queue replay and has equivalent JSON Lines and journal artifacts.
For causal next-open execution, an order-changing schedule entry's anchor received_at is no later
than the next slice start_at.
Audit journal¶
The journal JSON Schema validates each JSON Lines record.
Every record contains contract_version, engine_sequence, deterministic event_id, ordered
causation_ids, run_id, recorded_at, event_type, and an event-specific payload. Causal
references are unique prior event IDs from the same run. The version is repeated on every record
so a journal remains self-describing when it is streamed or split.
The first record is run_started with scenario_sha256 and the selected execution model. In v6,
initial_state then records the imported portfolio and reconciled valuation, followed by an
initial valuation; both precede the first market slice. The CLI hashes the exact batch document
or stream bytes it parses.
market_slice_received contains the complete normalized slice. Portfolio requests record their
basis, original weight when applicable, computed quantity, and sizing reference price. Orders use
eligible_after_slice_sequence; fills use slice_sequence. fill_clipped records the proposed
fill and the greatest lot-aligned permitted quantity. Its reason taxonomy version 1 names one of
max_order_quantity, max_long_position, max_short_position, max_gross_exposure,
max_leverage, initial_margin, or instrument_borrow_availability and carries a quantity, money,
ratio, or basis-points threshold.
Each order snapshot retains both creation and latest-update event IDs.
The journal also records split/dividend/distribution application, lifecycle transitions, action-driven order adjustments, observed borrow charges, recalls and close-outs, cash-interest entries, margin calls, liquidation-origin orders, and restoration. Every valuation contains complete per-currency cash attribution, signed per-instrument native and base-currency attribution, long, short, net, and gross exposure, execution and borrow fees, and its initial/maintenance margin snapshot. Those rows reconcile exactly to the aggregate valuation.
A successful replay ends with exactly one run_completed record containing the same scenario
hash, reconciled valuation, and mutually exclusive order-status counts. A journal without that
terminal record is incomplete. The requested journal path appears only after exclusive successful
finalization; a failed run retains the .partial artifact.
--durable-artifacts synchronizes staged contents before publication and containing-directory
metadata after publication and partial cleanup. The filesystem must support hard links plus file
and directory synchronization. Unsupported durability operations fail with artifact.io and do
not silently fall back to buffered publication.