From a4afe272bc23c9712fa9b29e63f07712d89b0c53 Mon Sep 17 00:00:00 2001 From: James Mallette Date: Sat, 22 Aug 2026 17:28:26 -0400 Subject: [PATCH] feat: add bounded order book replay --- CHANGELOG.md | 4 + README.md | 28 +- contracts/conformance/cases.json | 119 + contracts/conformance/manifest.json | 103 + contracts/strategy/v13/README.md | 61 + contracts/strategy/v13/dune | 15 + .../v13/fixtures/external.scenario.json | 308 +++ .../v13/fixtures/external.scenario.jsonl | 4 + .../v13/fixtures/external.strategy.jsonl | 14 + contracts/strategy/v13/message.schema.json | 302 ++ contracts/strategy/v13/transcript.schema.json | 82 + contracts/v15/README.md | 117 + contracts/v15/dune | 36 + contracts/v15/fixtures/demo.journal.jsonl | 29 + contracts/v15/fixtures/demo.scenario.json | 465 ++++ contracts/v15/fixtures/demo.scenario.jsonl | 6 + .../v15/fixtures/fill-clipped.journal.jsonl | 13 + .../v15/fixtures/fill-clipped.scenario.json | 273 ++ .../v15/fixtures/order-book.journal.jsonl | 13 + .../v15/fixtures/order-book.scenario.json | 378 +++ .../v15/fixtures/order-book.scenario.jsonl | 4 + .../v15/fixtures/quote-trade.journal.jsonl | 13 + .../v15/fixtures/quote-trade.scenario.json | 319 +++ .../v15/fixtures/quote-trade.scenario.jsonl | 4 + contracts/v15/journal.schema.json | 2427 +++++++++++++++++ contracts/v15/scenario-stream.schema.json | 78 + contracts/v15/scenario.schema.json | 870 ++++++ docs/api-reference.md | 2 +- docs/continuous-integration.md | 2 +- docs/execution-model.md | 18 +- docs/persistra.md | 8 +- docs/scenario.md | 24 +- lib/codec.ml | 97 +- lib/codec.mli | 1 + lib/contract.ml | 10 +- lib/engine.ml | 5 + lib/engine.mli | 11 + lib/execution.ml | 674 +++++ lib/execution.mli | 14 + lib/execution_model.ml | 39 +- lib/external_replay.ml | 6 +- lib/market_slice.ml | 35 +- lib/market_slice.mli | 18 + lib/order_book_event.ml | 132 + lib/order_book_event.mli | 77 + lib/replay.ml | 6 +- lib/scenario.ml | 203 +- lib/scenario_shape.ml | 28 +- lib/scenario_validation.ml | 6 +- lib/strategy_protocol.ml | 60 +- mkdocs.yml | 4 +- scripts/check-deterministic-journals | 16 + scripts/check-documentation.py | 4 +- scripts/release_artifacts.py | 12 +- test/cli.t | 2 +- test/dune | 116 +- test/test_diagnostic.ml | 3 +- test/test_domain.ml | 115 +- test/test_execution.ml | 396 +++ test/test_scenario.ml | 108 +- test/test_strategy_protocol.ml | 4 +- 61 files changed, 8150 insertions(+), 191 deletions(-) create mode 100644 contracts/strategy/v13/README.md create mode 100644 contracts/strategy/v13/dune create mode 100644 contracts/strategy/v13/fixtures/external.scenario.json create mode 100644 contracts/strategy/v13/fixtures/external.scenario.jsonl create mode 100644 contracts/strategy/v13/fixtures/external.strategy.jsonl create mode 100644 contracts/strategy/v13/message.schema.json create mode 100644 contracts/strategy/v13/transcript.schema.json create mode 100644 contracts/v15/README.md create mode 100644 contracts/v15/dune create mode 100644 contracts/v15/fixtures/demo.journal.jsonl create mode 100644 contracts/v15/fixtures/demo.scenario.json create mode 100644 contracts/v15/fixtures/demo.scenario.jsonl create mode 100644 contracts/v15/fixtures/fill-clipped.journal.jsonl create mode 100644 contracts/v15/fixtures/fill-clipped.scenario.json create mode 100644 contracts/v15/fixtures/order-book.journal.jsonl create mode 100644 contracts/v15/fixtures/order-book.scenario.json create mode 100644 contracts/v15/fixtures/order-book.scenario.jsonl create mode 100644 contracts/v15/fixtures/quote-trade.journal.jsonl create mode 100644 contracts/v15/fixtures/quote-trade.scenario.json create mode 100644 contracts/v15/fixtures/quote-trade.scenario.jsonl create mode 100644 contracts/v15/journal.schema.json create mode 100644 contracts/v15/scenario-stream.schema.json create mode 100644 contracts/v15/scenario.schema.json create mode 100644 lib/order_book_event.ml create mode 100644 lib/order_book_event.mli diff --git a/CHANGELOG.md b/CHANGELOG.md index b7aa900..d9352a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Add bounded level-two order-book replay with fresh snapshots, contiguous absolute updates, + multi-level marketable depth, deterministic passive queue position, and locked-book support. +- Publish scenario/journal contract v15 and external strategy protocol v13 while preserving v14 + and protocol v12 as frozen compatibility contracts. - Add causal quote/trade replay with displayed-liquidity capacity, aggressor-qualified passive fills, maker/taker fee attribution, and economic event timestamps. - Publish scenario/journal contract v14 and external strategy protocol v12 while preserving v13 diff --git a/README.md b/README.md index f307b7d..00905de 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,14 @@ scenario slices and scheduled or external intents fee-component attribution - Deterministic event IDs, ordered causal references, and order-creation attribution - Contract-selected compiled execution modules with versioned model-owned configuration and - capability descriptors; v13 adds conservative bar models and v14 adds causal quote/trade replay - while freezing `completed_bar_v1` + capability descriptors; v13 adds conservative bar models, v14 adds causal quote/trade replay, + and v15 adds bounded level-two order-book replay while freezing `completed_bar_v1` - Tick-aligned fixed-spread and participation-impact execution costs with separate reference, spread, impact, and final-price audit attribution - Causally ordered quotes and aggressor-classified trades with displayed-liquidity limits, maker/taker attribution, and event-time fills +- Bounded order-book snapshots and contiguous updates with price-time queue simulation, + multi-level depth consumption, partial fills, and locked-book support - Strict batch JSON and bounded-memory JSON Lines scenario parsing with JSON Schemas - Versioned synchronous JSON Lines strategy processes with per-request timeouts and strict lifecycle supervision @@ -96,7 +98,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v14/fixtures/demo.scenario.json \ + --input contracts/v15/fixtures/demo.scenario.json \ --validate-only ``` @@ -104,7 +106,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v14/fixtures/demo.scenario.json \ + --input contracts/v15/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -112,7 +114,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v14/fixtures/demo.scenario.jsonl \ + --input contracts/v15/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -121,7 +123,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v12/fixtures/external.scenario.json \ + --input contracts/strategy/v13/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -236,19 +238,19 @@ do not provide reducer snapshots or restart recovery. - [Diagnostic contract](docs/diagnostics.md) - [Scenario contract](docs/scenario.md) - [Contract conformance corpus](contracts/conformance/README.md) -- [Current contract v14 and conformance fixtures](contracts/v14/README.md) +- [Current contract v15 and conformance fixtures](contracts/v15/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v14/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v14/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v14/journal.schema.json) -- [External strategy protocol v12](contracts/strategy/v12/README.md) +- [Scenario JSON Schema](contracts/v15/scenario.schema.json) +- [Scenario stream record JSON Schema](contracts/v15/scenario-stream.schema.json) +- [Journal record JSON Schema](contracts/v15/journal.schema.json) +- [External strategy protocol v13](contracts/strategy/v13/README.md) - [Historical strategy protocol v3](contracts/strategy/v3/README.md) - [Historical strategy protocol v2](contracts/strategy/v2/README.md) - [Historical strategy protocol v1](contracts/strategy/v1/README.md) - [Persistra compatibility](docs/persistra.md) -- [Strategy message JSON Schema](contracts/strategy/v12/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v12/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v13/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v13/transcript.schema.json) - [Execution model](docs/execution-model.md) - [OCaml coverage](docs/coverage.md) - [Continuous integration and portability matrix](docs/continuous-integration.md) diff --git a/contracts/conformance/cases.json b/contracts/conformance/cases.json index 3b62267..0430ba0 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -921,6 +921,68 @@ "schema_expectation": "accept", "runtime_expectation": "accept", "rule": "structural" + }, + { + "name": "scenario-v15-valid", + "artifact": "scenario-v15", + "kind": "scenario", + "source": "v15/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-v15-order-book-valid", + "artifact": "scenario-v15", + "kind": "scenario", + "source": "v15/fixtures/order-book.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "scenario-stream-v15-valid", + "artifact": "scenario-stream-v15", + "kind": "scenario_stream", + "source": "v15/fixtures/order-book.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-ready-valid-v13", + "artifact": "strategy-message-v13", + "kind": "strategy_response", + "source": "strategy/v13/fixtures/external.strategy.jsonl", + "record": 2, + "extract": [ + "message" + ], + "expected_sequence": "1", + "protocol_version": "13", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" + }, + { + "name": "strategy-intents-valid-v13", + "artifact": "strategy-message-v13", + "kind": "strategy_response", + "source": "strategy/v13/fixtures/external.strategy.jsonl", + "record": 4, + "extract": [ + "message" + ], + "expected_sequence": "2", + "protocol_version": "13", + "mutations": [], + "schema_expectation": "accept", + "runtime_expectation": "accept", + "rule": "structural" } ], "schema_only_cases": [ @@ -1506,6 +1568,63 @@ }, "mutations": [], "schema_expectation": "accept" + }, + { + "name": "strategy-stopped-valid-v13", + "artifact": "strategy-message-v13", + "instance": { + "strategy_protocol_version": "13", + "strategy_sequence": "7", + "message_type": "stopped", + "payload": {} + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "stopped" + }, + { + "name": "strategy-error-valid-v13", + "artifact": "strategy-message-v13", + "instance": { + "strategy_protocol_version": "13", + "strategy_sequence": "7", + "message_type": "error", + "payload": { + "message": "fixture failure" + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v13-rejected-response-branch", + "artifact": "strategy-transcript-v13", + "instance": { + "strategy_diagnostic_version": "1", + "transcript_sequence": "2", + "record_type": "rejected_strategy_response", + "expected_strategy_sequence": "1", + "diagnostic": { + "diagnostic_version": "1", + "code": "strategy.protocol", + "phase": "strategy", + "message": "strategy initialization: invalid strategy response JSON", + "context": { + "json_path": "$", + "sequence": "1" + }, + "cause": null + }, + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": false + } + }, + "mutations": [], + "schema_expectation": "accept" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 6d5e637..c68ce46 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -1036,6 +1036,109 @@ "format": "jsonl" } ] + }, + { + "name": "scenario-v15", + "schema": "v15/scenario.schema.json", + "version_field": "contract_version", + "version": "15", + "sources": [ + { + "path": "v15/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v15/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "v15/fixtures/quote-trade.scenario.json", + "format": "json" + }, + { + "path": "v15/fixtures/order-book.scenario.json", + "format": "json" + }, + { + "path": "strategy/v13/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v15", + "schema": "v15/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "15", + "sources": [ + { + "path": "v15/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/quote-trade.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/order-book.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v13/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v15", + "schema": "v15/journal.schema.json", + "version_field": "contract_version", + "version": "15", + "sources": [ + { + "path": "v15/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/quote-trade.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v15/fixtures/order-book.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v13", + "schema": "strategy/v13/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "13", + "sources": [ + { + "path": "strategy/v13/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v13", + "schema": "strategy/v13/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "13", + "sources": [ + { + "path": "strategy/v13/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] } ] } diff --git a/contracts/strategy/v13/README.md b/contracts/strategy/v13/README.md new file mode 100644 index 0000000..bc7a8d5 --- /dev/null +++ b/contracts/strategy/v13/README.md @@ -0,0 +1,61 @@ +# External strategy protocol v13 + +Version 13 is a synchronous JSON Lines protocol over child-process standard input and output. +Trading Engine sends `initialize`, ordered `event` requests, and `shutdown`. The strategy answers +with `ready`, `intents`, and `stopped`. It may answer any request with `error`. +Protocol v11 remains available for scenario contract v13; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "13"` and a positive canonical +`strategy_sequence`. A response must repeat the sequence of its request. Only one request is +outstanding. Trading Engine rejects unknown or duplicate fields, invalid canonical values, +oversized lines, a wrong version or sequence, unexpected response types, EOF, timeout, and a +nonzero process exit. + +The event context contains the replay clock, a marked base-currency portfolio, deterministic group +exposure snapshots, all working orders, and the latest available bar for each instrument. Every +callback emitted for a market slice uses +that slice's `received_at` as `now` and uses its complete bars and FX vector. The portfolio reports +cash, equity, net, long, short, and gross market value plus every attributed cash ledger and +configured position. Position quantities and weights reflect applied fills. Weights are truncated +toward zero to six decimal places. `weights_available` is false and all weights are null when +equity is zero or negative. + +The `initialize` request identifies scenario contract v15 and includes the exact `initial_portfolio` +snapshot alongside the legacy cash projection. It also carries the complete versioned venue +calendars, nested execution configuration, financing policy, and settlement policy, so a strategy +can construct DAY orders and reject incompatible execution, financing, or settlement state before +replay. + +Matching pauses after each strategy callback. The engine applies the response against the exact +account and OMS state exposed by that callback before delivering another callback or considering +the next eligible order. Later same-slice contexts include the effects of earlier responses. The +eligible-order sequence is fixed at the start of matching, so newly submitted orders wait for a +later slice. Cancelling an order before its turn leaves its unused slice capacity available to the +next eligible order. + +Event payloads cover completed market slices with effective-time borrow and cash-rate observations +plus explicit settlement failures, fills, order updates, and rejected intents. Portfolio contexts +include cash-interest attribution and settled and unsettled cash and position quantities. Response +intents use the scenario v15 intent shapes. Market-slice events include lifecycle transitions and +the expanded corporate-action catalog, plus causally ordered quote/trade market events. +Protocol v13 also carries bounded order-book snapshots and incrementals and advertises the +`order_book_v1` configuration, including its maximum depth. + +External replay requires an empty batch schedule and empty streamed intent batches. The engine +records accepted messages in both directions in a deterministic transcript. A response rejected +for invalid JSON, fields, version, sequence, EOF, or size is never stored as an accepted exchange. +Instead, the partial transcript ends with a `rejected_strategy_response` diagnostic record. Version +1 rejection diagnostics use the shared +[`diagnostic/v1`](../../diagnostic/v1/README.md) contract. The transcript schema narrows that +contract to the `strategy.protocol` and `resource.limit` codes in the `strategy` phase. The record +includes the structured rejection diagnostic and at most the first 256 raw response bytes encoded +as lowercase hexadecimal. `observed_bytes` counts bytes available when the engine rejected the +response, and `truncated` reports whether the prefix omits observed bytes. The transcript and audit +journal retain partial files after failure and finalize only after their respective success checks. + +- `message.schema.json` validates individual requests and responses. +- `transcript.schema.json` validates accepted exchanges and rejected-response diagnostics. +- `fixtures/external.scenario.json` is the batch replay fixture. +- `fixtures/external.scenario.jsonl` is its bounded-memory stream form. +- `fixtures/external.strategy.jsonl` is the canonical protocol transcript. diff --git a/contracts/strategy/v13/dune b/contracts/strategy/v13/dune new file mode 100644 index 0000000..70ad1c7 --- /dev/null +++ b/contracts/strategy/v13/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v13/message.schema.json) + (transcript.schema.json as contracts/strategy/v13/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v13/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v13/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v13/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v13/fixtures/external.scenario.json b/contracts/strategy/v13/fixtures/external.scenario.json new file mode 100644 index 0000000..5d45ea2 --- /dev/null +++ b/contracts/strategy/v13/fixtures/external.scenario.json @@ -0,0 +1,308 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "strategy-protocol-fixture" + }, + "run_id": "external-demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 0, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "external-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [], + "slices": [ + { + "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": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "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": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 0, + "debit_rate_bps": 0 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/strategy/v13/fixtures/external.scenario.jsonl b/contracts/strategy/v13/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..15ab15e --- /dev/null +++ b/contracts/strategy/v13/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"strategy-protocol-fixture"},"run_id":"external-demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","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":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"15","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v13/fixtures/external.strategy.jsonl b/contracts/strategy/v13/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..13c41e1 --- /dev/null +++ b/contracts/strategy/v13/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"13","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"15","scenario_sha256":"6809a3638fe668a2a11e56c42e9bac7e506c0064eb2cb0a3e71ba09d92839ee7","run_id":"external-demo","base_currency":"USD","initial_cash":[{"currency":"USD","amount":"10000"}],"initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00.000000Z","closes_at":"2026-01-02T14:25:00.000000Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00.000000Z","closes_at":"2026-01-02T14:30:00.000000Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00.000000Z","closes_at":"2026-01-02T20:55:00.000000Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00.000000Z","closes_at":"2026-01-02T21:00:00.000000Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00.000000Z","closes_at":"2026-01-03T01:00:00.000000Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00.000000Z","closes_at":"2026-01-05T21:00:00.000000Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00.000000Z","closes_at":"2026-01-06T21:00:00.000000Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00.000000Z","closes_at":"2026-01-07T21:00:00.000000Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00.000000Z","closes_at":"2026-01-08T18:00:00.000000Z"}]}]}],"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":0,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_v1","configuration":{"version":"2","participation_bps":5000,"fee_schedules":[{"schedule_id":"external-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"any"}]}]}},"metadata":{"producer":"strategy-protocol-fixture"}}}} +{"strategy_protocol_version":"13","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"13","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"2","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"2","message_type":"intents","payload":{"intents":[{"type":"target_quantities","targets":[{"instrument_id":"demo-equity-acme","quantity":"2"}]},{"type":"emit_metric","name":"fixture_signal","value":"2"}]}}} +{"strategy_protocol_version":"13","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"3","message_type":"event","payload":{"context":{"now":"2026-01-02T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"10000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","equity":"10000","weights_available":true,"cash_weight":"1","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"0","mark":"104","base_market_value":"0","weight":"0","settled_quantity":"0","unsettled_quantity":"0"}],"group_exposures":[]},"working_orders":[{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"4","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"fill_received","fill":{"fill_id":"external-demo-fill-000000000001","order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2","price":"103","notional":"206","fee":"0.456","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.206","quote_amount":"0.206"}]}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"5","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"order_updated","order":{"order_id":"external-demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"2","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"external-demo-event-000000000007","updated_event_id":"external-demo-event-000000000007","created_sequence":"7","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"2","filled_notional":"206","status":"filled","rejection_reason":null}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"6","message_type":"event","payload":{"context":{"now":"2026-01-05T21:00:02.000000Z","portfolio":{"base_currency":"USD","cash":"9793.544","net_market_value":"214","long_market_value":"214","short_market_value":"0","gross_exposure":"214","equity":"10007.544","weights_available":true,"cash_weight":"0.978616","cash_balances":[{"currency":"USD","amount":"9793.544","fx_rate":"1","base_value":"9793.544","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"-206.456","base_settled_value":"10000","base_unsettled_value":"-206.456"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"2","mark":"107","base_market_value":"214","weight":"0.021383","settled_quantity":"0","unsettled_quantity":"2"}],"group_exposures":[]},"working_orders":[],"latest_bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}]},"event":{"type":"market_slice_closed","market_slice":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":0,"debit_rate_bps":0}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}}}}} +{"strategy_protocol_version":"13","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"13","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"13","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"13","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"13","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v13/message.schema.json b/contracts/strategy/v13/message.schema.json new file mode 100644 index 0000000..ac9b452 --- /dev/null +++ b/contracts/strategy/v13/message.schema.json @@ -0,0 +1,302 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/message.schema.json", + "title": "Trading Engine external strategy protocol v13 message", + "description": "One strict request or response in the synchronous JSON Lines strategy protocol. The engine additionally enforces direction, sequence pairing, canonical values, response limits, and lifecycle order.", + "oneOf": [ + { "$ref": "#/$defs/initialize" }, + { "$ref": "#/$defs/ready" }, + { "$ref": "#/$defs/event" }, + { "$ref": "#/$defs/intents" }, + { "$ref": "#/$defs/shutdown" }, + { "$ref": "#/$defs/stopped" }, + { "$ref": "#/$defs/error" } + ], + "$defs": { + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "base": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "strategy_sequence", "message_type", "payload"], + "properties": { + "strategy_protocol_version": { "const": "13" }, + "strategy_sequence": { "$ref": "#/$defs/sequence" }, + "message_type": { "type": "string" }, + "payload": { "type": "object" } + } + }, + "initialize": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "initialize" }, + "payload": { "$ref": "#/$defs/initializePayload" } + } + } + ] + }, + "initializePayload": { + "type": "object", + "additionalProperties": false, + "required": ["engine_version", "scenario_contract_version", "scenario_sha256", "run_id", "base_currency", "initial_cash", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "metadata"], + "properties": { + "engine_version": { "type": "string", "minLength": 1 }, + "scenario_contract_version": { "const": "15" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlement" }, + "metadata": { "type": "object" } + } + }, + "ready": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "ready" }, + "payload": { "$ref": "#/$defs/readyPayload" } + } + } + ] + }, + "readyPayload": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_name", "strategy_version"], + "properties": { + "strategy_name": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "strategy_version": { + "oneOf": [ + { "type": "null" }, + { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + ] + } + } + }, + "event": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "event" }, + "payload": { "$ref": "#/$defs/eventPayload" } + } + } + ] + }, + "eventPayload": { + "type": "object", + "additionalProperties": false, + "required": ["context", "event"], + "properties": { + "context": { "$ref": "#/$defs/context" }, + "event": { "$ref": "#/$defs/strategyEvent" } + } + }, + "context": { + "type": "object", + "additionalProperties": false, + "required": ["now", "portfolio", "working_orders", "latest_bars"], + "properties": { + "now": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/bar" } + } + } + }, + "portfolio": { + "type": "object", + "additionalProperties": false, + "required": ["base_currency", "cash", "net_market_value", "long_market_value", "short_market_value", "gross_exposure", "equity", "weights_available", "cash_weight", "cash_balances", "positions", "group_exposures"], + "properties": { + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "weights_available": { "type": "boolean" }, + "cash_weight": { "$ref": "#/$defs/optionalWeight" }, + "cash_balances": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/cashAttribution" } + }, + "positions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/markedPosition" } + }, + "group_exposures": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" } + ] + }, + "markedPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "settled_quantity", "unsettled_quantity", "mark", "base_market_value", "weight"], + "properties": { + "instrument_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/signedDecimal" }, + "weight": { "$ref": "#/$defs/optionalWeight" } + } + }, + "strategyEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "market_slice"], + "properties": { + "type": { "const": "market_slice_closed" }, + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketSlice" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "fill"], + "properties": { + "type": { "const": "fill_received" }, + "fill": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/fill" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "order"], + "properties": { + "type": { "const": "order_updated" }, + "order": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json#/$defs/order" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "reason"], + "properties": { + "type": { "const": "intent_rejected" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + }, + "intents": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "intents" }, + "payload": { "$ref": "#/$defs/intentsPayload" } + } + } + ] + }, + "intentsPayload": { + "type": "object", + "additionalProperties": false, + "required": ["intents"], + "properties": { + "intents": { + "type": "array", + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/intent" } + } + } + }, + "shutdown": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "shutdown" }, + "payload": { "$ref": "#/$defs/emptyPayload" } + } + } + ] + }, + "stopped": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "stopped" }, + "payload": { "$ref": "#/$defs/emptyPayload" } + } + } + ] + }, + "emptyPayload": { + "type": "object", + "additionalProperties": false, + "maxProperties": 0 + }, + "error": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "message_type": { "const": "error" }, + "payload": { "$ref": "#/$defs/errorPayload" } + } + } + ] + }, + "errorPayload": { + "type": "object", + "additionalProperties": false, + "required": ["message"], + "properties": { + "message": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + } + } +} diff --git a/contracts/strategy/v13/transcript.schema.json b/contracts/strategy/v13/transcript.schema.json new file mode 100644 index 0000000..031b51b --- /dev/null +++ b/contracts/strategy/v13/transcript.schema.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/transcript.schema.json", + "title": "Trading Engine external strategy protocol v13 transcript record", + "description": "One accepted exchange or rejected-response diagnostic retained from a supervised stdio strategy session.", + "oneOf": [ + { "$ref": "#/$defs/exchange" }, + { "$ref": "#/$defs/rejectedResponse" } + ], + "$defs": { + "canonicalSequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "exchange": { + "type": "object", + "additionalProperties": false, + "required": ["strategy_protocol_version", "transcript_sequence", "direction", "message"], + "properties": { + "strategy_protocol_version": { "const": "13" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v13/message.schema.json" + } + } + }, + "rejectedResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "strategy_diagnostic_version", + "transcript_sequence", + "record_type", + "expected_strategy_sequence", + "diagnostic", + "evidence" + ], + "properties": { + "strategy_diagnostic_version": { "const": "1" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "record_type": { "const": "rejected_strategy_response" }, + "expected_strategy_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "diagnostic": { "$ref": "#/$defs/diagnostic" }, + "evidence": { "$ref": "#/$defs/evidence" } + } + }, + "diagnostic": { + "allOf": [ + { + "$ref": "https://github.com/fallblu/trading-engine/contracts/diagnostic/v1/diagnostic.schema.json" + }, + { + "properties": { + "code": { "enum": ["strategy.protocol", "resource.limit"] }, + "phase": { "const": "strategy" } + } + } + ] + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["encoding", "prefix", "observed_bytes", "truncated"], + "properties": { + "encoding": { "const": "hex" }, + "prefix": { + "type": "string", + "pattern": "^(?:[0-9a-f]{2}){0,256}$" + }, + "observed_bytes": { + "type": "integer", + "minimum": 0, + "maximum": 1048577 + }, + "truncated": { "type": "boolean" } + } + } + } +} diff --git a/contracts/v15/README.md b/contracts/v15/README.md new file mode 100644 index 0000000..a1917b8 --- /dev/null +++ b/contracts/v15/README.md @@ -0,0 +1,117 @@ +# Trading Engine contract v15 + +This directory is the authoritative v15 process and file contract shared by Trading Engine and its +clients. Versions 14 through 3 remain readable during their client transitions. + +- `scenario.schema.json` validates batch replay inputs. +- `scenario-stream.schema.json` validates each JSON Lines scenario-stream record. +- `journal.schema.json` validates each JSON Lines audit record. +- The files under `fixtures/` form the canonical valid conformance corpus. +- `fill-clipped.scenario.json` and its journal exercise a leverage-limited partial fill. + +Version 7 requires exactly one explicit risk policy per catalog instrument. Each policy defines +order, signed-position, notional, initial-margin, maintenance-margin, and shorting limits. Versioned +risk groups have explicit membership, may overlap, and can constrain gross, long, short, absolute +net, and gross-to-equity concentration exposure. + +Runtime validation requires exact currency, position-mark, and FX coverage; known instruments; +lot-aligned quantities; tick-aligned positive marks; basis with the same sign as quantity; +nonnegative fee histories; the base FX rate equal to one; instrument, group, aggregate exposure, +leverage, and initial-margin limits. Signed cash is valid. A successful v15 run emits `initial_state` +immediately after `run_started`, followed by a reconciled initial `valuation`, before market data. + +Admission and fill clipping include working-order reservations. When multiple groups limit the same +fill, lexical group identity is the deterministic tie breaker. Valuations and strategy contexts +carry group exposure snapshots, and clipping thresholds identify the exact instrument or group. + +Every v15 scenario, stream record, and journal record carries `"contract_version": "15"`. + +Version 8 adds explicit `market`, `limit`, `stop`, and `stop_limit` orders with `gtc`, `ioc`, +`fok`, `day`, and `gtd` time-in-force policies. `day` orders identify both their venue and the +exact versioned calendar; `gtd` orders carry an absolute expiry timestamp. Older contracts retain +their frozen mapping: market orders are IOC and limit orders are GTC. + +Stops evaluate only completed OHLCV bars. A gap through the trigger records the bar start as the +trigger time; an intrabar touch records the bar end. Trigger state and slice sequence are journaled, +and an activated order cannot execute before the following slice. A stop becomes a market order; +a stop-limit becomes its configured limit order. Splits adjust both trigger and limit prices. + +IOC orders cancel any remainder after their first eligible slice. FOK orders fill only when the +full remaining quantity fits both execution capacity and risk capacity, otherwise they cancel with +no fill. DAY orders cancel after matching the slice that reaches the selected session's final +phase close. GTD orders cancel before matching any completed bar whose end reaches or passes the +expiry, avoiding ambiguous partial-bar execution. + +The v10 `execution` object uses `completed_bar_v1` configuration version `"2"`: participation basis +points plus exactly one composable fee schedule per instrument. Named fixed, notional-basis-point, +and per-unit components declare currency, rounding, and maker/taker applicability. Optional +per-fill minimums and caps use the schedule settlement currency; negative components represent +rebates. Fills and valuations retain every native, quote, and base-currency attribution. Runtime +capabilities also advertise frozen configuration version `"1"` for older scenario contracts. + +Version 10 adds a required `financing` policy and effective-time observations on every market +slice. Borrow observations provide per-instrument locate availability, signed annual rates, and +recall state. Cash observations provide separate annual credit and debit rates per currency. +Policies select Actual/365 or Actual/360 day count, simple or daily compounding, missing-data +handling, locate rejection or fill clipping, and recall rejection or deterministic close-out. + +Borrow availability is enforced when a fill would create or increase a short. Recalls cancel +active sells and may submit priority IOC covers until the position is flat. Borrow charges and cash +interest use the exact slice interval, update native ledgers deterministically, and emit dedicated +journal records. Valuations report cash interest separately and include it in aggregate realized +P&L. Version 9 and earlier retain their frozen fixed-borrow behavior and wire shapes. + +Version 12 separates trade-date economic accounting from settlement-date availability. A required +settlement policy selects total or settled cash buying power and total or settled position +availability. Versioned calendars enumerate canonical business dates, and each instrument has an +explicit business-day lag. Every fill creates a deterministic settlement instruction containing +its cash and position movements, trade date, and due date. A due instruction either settles on the +first eligible slice or records a named failure supplied by that slice. + +Valuations and strategy contexts report settled and unsettled cash and quantities without changing +economic equity. Journals include instruction-created, completed, and failed events. Scenario v10 +and strategy protocol v8 retain their frozen immediate-settlement wire behavior. + +Version 12 adds exact stock-dividend, rights, and spin-off distributions. Each distribution names +its destination instrument, exact entitlement ratio, basis allocation in basis points, and either +rejects fractional entitlements or converts them to cash at an explicit price and currency. +Stock dividends adjust persistent targets and eligible working orders; every distribution journals +delivered quantity, fractional quantity, allocated basis, fractional basis, and cash in lieu. + +Lifecycle events keep stable instrument identity separate from mutable symbol and provider +mappings. Halt and resume transitions control tradability. Expiration and delisting are terminal, +cancel active orders, clear target exposure, and require an explicit hold or cash-out policy. +Cash-out specifies its terminal price and currency. Every transition journals the source event, +resulting listing state, provider provenance, liquidated quantity, and cash attribution. + +Version 13 adds `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1` without changing +the frozen `completed_bar_v1` semantics. Next-open limits require a marketable later open; +adverse-touch limits require a one-tick trade-through before a maker fill is eligible. Both models +declare fixed half-spread and linear participation-impact catalogs, including an explicit policy +for missing bar volume. Price costs round away from the reference price to instrument ticks and +cannot violate a limit. An `execution_price_selected` audit record attributes the reference price, +spread adjustment, impact adjustment, and final executable price before each fill. + +Version 14 adds `quote_trade_v1` and causally ordered `market_events`. Quotes expose bid/ask price +and displayed size. Trades expose price, size, and buy, sell, or unknown aggressor side. Each event +records economic, availability, and receipt timestamps plus a positive ingest sequence. Replay +orders events by availability, receipt, and ingest sequence. Marketable orders consume only +displayed quote liquidity; passive orders require appropriately aggressed trade evidence, and an +unknown aggressor never fills them. Event capacity is shared deterministically across order +priority and fills retain the event's economic timestamp. Completed bars remain the valuation +boundary. The `quote-trade` batch, stream, and journal fixtures demonstrate equivalent replay. + +Version 15 adds `order_book_v1` and bounded level-two `order_book_events`. Every per-instrument +slice bundle starts with a complete snapshot and continues with contiguous absolute set, delete, +and aggressor-classified trade updates. Snapshots and updates reject crossed books, missing +deletes, sequence gaps, tick or lot misalignment, and depth beyond the configured limit; locked +books are valid. State is rebuilt from each slice snapshot, so replay never depends on hidden data +from a prior slice. + +Marketable orders walk observable opposite-side depth in price priority. Passive limit orders join +behind displayed same-price quantity and earlier engine orders. Reductions decrease quantity ahead, +adds join behind, and only appropriately aggressed trades consume the queue and fill the order. +Partial fills and cancellations therefore remain deterministic. Book liquidity is independent of +bar and quote/trade execution semantics, while completed bars remain the valuation boundary. The +`order-book` batch, stream, and journal fixtures demonstrate cancellation, queue depletion, maker +fills, bounded state, and batch/stream equivalence. diff --git a/contracts/v15/dune b/contracts/v15/dune new file mode 100644 index 0000000..d1742ac --- /dev/null +++ b/contracts/v15/dune @@ -0,0 +1,36 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v15/journal.schema.json) + (scenario-stream.schema.json as contracts/v15/scenario-stream.schema.json) + (scenario.schema.json as contracts/v15/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v15/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v15/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v15/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v15/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v15/fixtures/fill-clipped.scenario.json) + (fixtures/quote-trade.journal.jsonl + as + contracts/v15/fixtures/quote-trade.journal.jsonl) + (fixtures/quote-trade.scenario.json + as + contracts/v15/fixtures/quote-trade.scenario.json) + (fixtures/quote-trade.scenario.jsonl + as + contracts/v15/fixtures/quote-trade.scenario.jsonl) + (fixtures/order-book.journal.jsonl + as + contracts/v15/fixtures/order-book.journal.jsonl) + (fixtures/order-book.scenario.json + as + contracts/v15/fixtures/order-book.scenario.json) + (fixtures/order-book.scenario.jsonl + as + contracts/v15/fixtures/order-book.scenario.jsonl))) diff --git a/contracts/v15/fixtures/demo.journal.jsonl b/contracts/v15/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..c14e704 --- /dev/null +++ b/contracts/v15/fixtures/demo.journal.jsonl @@ -0,0 +1,29 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"demo-event-000000000001","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"068f28c64905f5847ed3ecfac808940c3f1ba43e0d198d96f11929ba234703bc","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"demo-event-000000000002","causation_ids":["demo-event-000000000001"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"demo-event-000000000003","causation_ids":["demo-event-000000000002"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000","net_market_value":"100","long_market_value":"100","short_market_value":"0","gross_exposure":"100","cost_basis":"90","realized_pnl":"5","unrealized_pnl":"10","equity":"10100","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000","fx_rate":"1","base_value":"10000","interest":"0","base_interest":"0","settled_amount":"10000","unsettled_amount":"0","base_settled_value":"10000","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"100","fx_rate":"1","market_value":"100","base_market_value":"100","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"10","base_unrealized_pnl":"10","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0","settled_cash":"10000","unsettled_cash":"0","margin":{"initial_requirement":"50","maintenance_requirement":"25","initial_excess":"10050","maintenance_excess":"10075","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"demo-event-000000000004","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-01-02T14:30:00.000000Z","end_at":"2026-01-02T21:00:00.000000Z","available_at":"2026-01-02T21:00:01.000000Z","received_at":"2026-01-02T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"100","high":"105","low":"99","close":"104","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"5","event_id":"demo-event-000000000005","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-02T14:30:00.000000Z","period_end":"2026-01-02T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.074201"}} +{"contract_version":"15","engine_sequence":"6","event_id":"demo-event-000000000006","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"weights","targets":[{"instrument_id":"demo-equity-acme","weight":"0.1","quantity":"9.715","reference_price":"104"}]}} +{"contract_version":"15","engine_sequence":"7","event_id":"demo-event-000000000007","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"metric_emitted","payload":{"name":"desired_weight","value":"0.1"}} +{"contract_version":"15","engine_sequence":"8","event_id":"demo-event-000000000008","causation_ids":["demo-event-000000000004","demo-event-000000000006"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"9","event_id":"demo-event-000000000009","causation_ids":["demo-event-000000000004"],"run_id":"demo","recorded_at":"2026-01-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"10000.074201","net_market_value":"104","long_market_value":"104","short_market_value":"0","gross_exposure":"104","cost_basis":"90","realized_pnl":"5.074201","unrealized_pnl":"14","equity":"10104.074201","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25","total_fees":"0.75","cash_balances":[{"currency":"USD","amount":"10000.074201","fx_rate":"1","base_value":"10000.074201","interest":"0.074201","base_interest":"0.074201","settled_amount":"10000.074201","unsettled_amount":"0","base_settled_value":"10000.074201","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"1","mark":"104","fx_rate":"1","market_value":"104","base_market_value":"104","cost_basis":"90","base_cost_basis":"90","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"14","base_unrealized_pnl":"14","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"0.5","base_execution_fees":"0.5","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"0.75","base_total_fees":"0.75","execution_fee_components":[],"settled_quantity":"1","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.074201","settled_cash":"10000.074201","unsettled_cash":"0","margin":{"initial_requirement":"52","maintenance_requirement":"26","initial_excess":"10052.074201","maintenance_excess":"10078.074201","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"10","event_id":"demo-event-000000000010","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-01-05T14:30:00.000000Z","end_at":"2026-01-05T21:00:00.000000Z","available_at":"2026-01-05T21:00:01.000000Z","received_at":"2026-01-05T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"103","high":"108","low":"102","close":"107","volume":"13"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"11","event_id":"demo-event-000000000011","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-05T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"10000.074201","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-05T14:30:00.000000Z","period_end":"2026-01-05T21:00:00.000000Z","amount":"0.074201","closing_balance":"10000.148402"}} +{"contract_version":"15","engine_sequence":"12","event_id":"demo-event-000000000012","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","reference_price":"103","spread_adjustment":"0.06","impact_adjustment":"0.13","final_price":"103.19"}} +{"contract_version":"15","engine_sequence":"13","event_id":"demo-event-000000000013","causation_ids":["demo-event-000000000012"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000001","order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"6.5","price":"103.19","notional":"670.735","fee":"0.920735","executed_at":"2026-01-05T14:30:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_amount":"0.670735"}]}} +{"contract_version":"15","engine_sequence":"14","event_id":"demo-event-000000000014","causation_ids":["demo-event-000000000008","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"demo-order-000000000001","instrument_id":"demo-equity-acme","side":"buy","quantity":"8.715","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000008","updated_event_id":"demo-event-000000000008","created_sequence":"8","created_at":"2026-01-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"6.5","filled_notional":"670.735","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"15","engine_sequence":"15","event_id":"demo-event-000000000015","causation_ids":["demo-event-000000000006","demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","quantity":"2.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000015","updated_event_id":"demo-event-000000000015","created_sequence":"15","created_at":"2026-01-05T21:00:02.000000Z","eligible_after_slice_sequence":"2","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"16","event_id":"demo-event-000000000016","causation_ids":["demo-event-000000000010"],"run_id":"demo","recorded_at":"2026-01-05T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9328.492667","net_market_value":"802.5","long_market_value":"802.5","short_market_value":"0","gross_exposure":"802.5","cost_basis":"761.655735","realized_pnl":"5.148402","unrealized_pnl":"40.844265","equity":"10130.992667","dividend_pnl":"1","execution_fees":"1.420735","borrow_fees":"0.25","total_fees":"1.670735","cash_balances":[{"currency":"USD","amount":"9328.492667","fx_rate":"1","base_value":"9328.492667","interest":"0.148402","base_interest":"0.148402","settled_amount":"9328.492667","unsettled_amount":"0","base_settled_value":"9328.492667","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"7.5","mark":"107","fx_rate":"1","market_value":"802.5","base_market_value":"802.5","cost_basis":"761.655735","base_cost_basis":"761.655735","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"40.844265","base_unrealized_pnl":"40.844265","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.420735","base_execution_fees":"1.420735","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"1.670735","base_total_fees":"1.670735","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"settled_quantity":"7.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_currency":"USD","quote_amount":"0.25","base_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.670735","quote_currency":"USD","quote_amount":"0.670735","base_amount":"0.670735"}],"cash_interest":"0.148402","settled_cash":"9328.492667","unsettled_cash":"0","margin":{"initial_requirement":"401.25","maintenance_requirement":"200.625","initial_excess":"9729.742667","maintenance_excess":"9930.367667","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"17","event_id":"demo-event-000000000017","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"3","start_at":"2026-01-06T14:30:00.000000Z","end_at":"2026-01-06T21:00:00.000000Z","available_at":"2026-01-06T21:00:01.000000Z","received_at":"2026-01-06T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"107","high":"109","low":"104","close":"105","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"18","event_id":"demo-event-000000000018","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-06T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9328.492667","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-06T14:30:00.000000Z","period_end":"2026-01-06T21:00:00.000000Z","amount":"0.069218","closing_balance":"9328.561885"}} +{"contract_version":"15","engine_sequence":"19","event_id":"demo-event-000000000019","causation_ids":["demo-event-000000000015","demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","side":"buy","reference_price":"107","spread_adjustment":"0.06","impact_adjustment":"0.01","final_price":"107.07"}} +{"contract_version":"15","engine_sequence":"20","event_id":"demo-event-000000000020","causation_ids":["demo-event-000000000019"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000002","order_id":"demo-order-000000000002","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"buy","quantity":"2.215","price":"107.07","notional":"237.16005","fee":"0.487161","executed_at":"2026-01-06T14:30:00.000000Z","slice_sequence":"3","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.237161","quote_amount":"0.237161"}]}} +{"contract_version":"15","engine_sequence":"21","event_id":"demo-event-000000000021","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"target_portfolio_requested","payload":{"basis":"quantities","targets":[{"instrument_id":"demo-equity-acme","weight":null,"quantity":"2.5","reference_price":null}]}} +{"contract_version":"15","engine_sequence":"22","event_id":"demo-event-000000000022","causation_ids":["demo-event-000000000017","demo-event-000000000021"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","quantity":"7.215","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"target_rebalance","created_event_id":"demo-event-000000000022","updated_event_id":"demo-event-000000000022","created_sequence":"22","created_at":"2026-01-06T21:00:02.000000Z","eligible_after_slice_sequence":"3","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"23","event_id":"demo-event-000000000023","causation_ids":["demo-event-000000000017"],"run_id":"demo","recorded_at":"2026-01-06T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9090.914674","net_market_value":"1020.075","long_market_value":"1020.075","short_market_value":"0","gross_exposure":"1020.075","cost_basis":"999.302946","realized_pnl":"5.21762","unrealized_pnl":"20.772054","equity":"10110.989674","dividend_pnl":"1","execution_fees":"1.907896","borrow_fees":"0.25","total_fees":"2.157896","cash_balances":[{"currency":"USD","amount":"9090.914674","fx_rate":"1","base_value":"9090.914674","interest":"0.21762","base_interest":"0.21762","settled_amount":"9090.914674","unsettled_amount":"0","base_settled_value":"9090.914674","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"9.715","mark":"105","fx_rate":"1","market_value":"1020.075","base_market_value":"1020.075","cost_basis":"999.302946","base_cost_basis":"999.302946","realized_pnl":"5","base_realized_pnl":"5","unrealized_pnl":"20.772054","base_unrealized_pnl":"20.772054","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"1.907896","base_execution_fees":"1.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"2.157896","base_total_fees":"2.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"settled_quantity":"9.715","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.5","quote_currency":"USD","quote_amount":"0.5","base_amount":"0.5"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.907896","quote_currency":"USD","quote_amount":"0.907896","base_amount":"0.907896"}],"cash_interest":"0.21762","settled_cash":"9090.914674","unsettled_cash":"0","margin":{"initial_requirement":"510.0375","maintenance_requirement":"255.01875","initial_excess":"9600.952174","maintenance_excess":"9855.970924","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"24","event_id":"demo-event-000000000024","causation_ids":[],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"4","start_at":"2026-01-07T14:30:00.000000Z","end_at":"2026-01-07T21:00:00.000000Z","available_at":"2026-01-07T21:00:01.000000Z","received_at":"2026-01-07T21:00:02.000000Z","bars":[{"instrument_id":"demo-equity-acme","open":"105","high":"107","low":"103","close":"106","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"25","event_id":"demo-event-000000000025","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-01-07T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"9090.914674","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-01-07T14:30:00.000000Z","period_end":"2026-01-07T21:00:00.000000Z","amount":"0.067455","closing_balance":"9090.982129"}} +{"contract_version":"15","engine_sequence":"26","event_id":"demo-event-000000000026","causation_ids":["demo-event-000000000022","demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"execution_price_selected","payload":{"order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","side":"sell","reference_price":"105","spread_adjustment":"0.06","impact_adjustment":"0.02","final_price":"104.92"}} +{"contract_version":"15","engine_sequence":"27","event_id":"demo-event-000000000027","causation_ids":["demo-event-000000000026"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"demo-fill-000000000003","order_id":"demo-order-000000000003","instrument_id":"demo-equity-acme","quote_currency":"USD","side":"sell","quantity":"7.215","price":"104.92","notional":"756.9978","fee":"1","executed_at":"2026-01-07T14:30:00.000000Z","slice_sequence":"4","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.25","quote_amount":"0.25"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"0.756998","quote_amount":"0.756998"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_amount":"-0.006998"}]}} +{"contract_version":"15","engine_sequence":"28","event_id":"demo-event-000000000028","causation_ids":["demo-event-000000000024"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"29","event_id":"demo-event-000000000029","causation_ids":["demo-event-000000000028"],"run_id":"demo","recorded_at":"2026-01-07T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"068f28c64905f5847ed3ecfac808940c3f1ba43e0d198d96f11929ba234703bc","execution_model":"completed_bar_adverse_touch_v1","valuation":{"base_currency":"USD","cash":"9846.979929","net_market_value":"265","long_market_value":"265","short_market_value":"0","gross_exposure":"265","cost_basis":"257.154644","realized_pnl":"19.134573","unrealized_pnl":"7.845356","equity":"10111.979929","dividend_pnl":"1","execution_fees":"2.907896","borrow_fees":"0.25","total_fees":"3.157896","cash_balances":[{"currency":"USD","amount":"9846.979929","fx_rate":"1","base_value":"9846.979929","interest":"0.285075","base_interest":"0.285075","settled_amount":"9846.979929","unsettled_amount":"0","base_settled_value":"9846.979929","base_unsettled_value":"0"}],"positions":[{"instrument_id":"demo-equity-acme","quote_currency":"USD","quantity":"2.5","mark":"106","fx_rate":"1","market_value":"265","base_market_value":"265","cost_basis":"257.154644","base_cost_basis":"257.154644","realized_pnl":"18.849498","base_realized_pnl":"18.849498","unrealized_pnl":"7.845356","base_unrealized_pnl":"7.845356","dividend_pnl":"1","base_dividend_pnl":"1","execution_fees":"2.907896","base_execution_fees":"2.907896","borrow_fees":"0.25","base_borrow_fees":"0.25","total_fees":"3.157896","base_total_fees":"3.157896","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"settled_quantity":"2.5","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"0.75","quote_currency":"USD","quote_amount":"0.75","base_amount":"0.75"},{"name":"exchange","kind":"notional_bps","currency":"USD","amount":"1.664894","quote_currency":"USD","quote_amount":"1.664894","base_amount":"1.664894"},{"name":"maximum_adjustment","kind":"maximum_adjustment","currency":"USD","amount":"-0.006998","quote_currency":"USD","quote_amount":"-0.006998","base_amount":"-0.006998"}],"cash_interest":"0.285075","settled_cash":"9846.979929","unsettled_cash":"0","margin":{"initial_requirement":"132.5","maintenance_requirement":"66.25","initial_excess":"9979.479929","maintenance_excess":"10045.729929","margin_call":false},"group_exposures":[]},"order_counts":{"total":3,"active":0,"filled":2,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/demo.scenario.json b/contracts/v15/fixtures/demo.scenario.json new file mode 100644 index 0000000..b2f17e1 --- /dev/null +++ b/contracts/v15/fixtures/demo.scenario.json @@ -0,0 +1,465 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine-demo", + "purpose": "deterministic conformance fixture" + }, + "run_id": "demo", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "10000" + } + ], + "positions": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "1", + "cost_basis": "90", + "realized_pnl": "5", + "dividend_pnl": "1", + "execution_fees": "0.5", + "borrow_fees": "0.25" + } + ], + "marks": [ + { + "instrument_id": "demo-equity-acme", + "price": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "demo-equity-acme", + "symbol": "ACME", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "0.001" + } + ], + "venue_calendars": [ + { + "calendar_id": "demo-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "demo-equity-acme" + ], + "sessions": [ + { + "session_date": "2026-01-01", + "policy": "holiday", + "phases": [] + }, + { + "session_date": "2026-01-02", + "policy": "regular", + "phases": [ + { + "phase": "premarket", + "opens_at": "2026-01-02T09:00:00Z", + "closes_at": "2026-01-02T14:25:00Z" + }, + { + "phase": "opening_auction", + "opens_at": "2026-01-02T14:25:00Z", + "closes_at": "2026-01-02T14:30:00Z" + }, + { + "phase": "regular", + "opens_at": "2026-01-02T14:30:00Z", + "closes_at": "2026-01-02T20:55:00Z" + }, + { + "phase": "closing_auction", + "opens_at": "2026-01-02T20:55:00Z", + "closes_at": "2026-01-02T21:00:00Z" + }, + { + "phase": "postmarket", + "opens_at": "2026-01-02T21:00:00Z", + "closes_at": "2026-01-03T01:00:00Z" + } + ] + }, + { + "session_date": "2026-01-05", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-05T14:30:00Z", + "closes_at": "2026-01-05T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-06", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-06T14:30:00Z", + "closes_at": "2026-01-06T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-07", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-07T14:30:00Z", + "closes_at": "2026-01-07T21:00:00Z" + } + ] + }, + { + "session_date": "2026-01-08", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-01-08T14:30:00Z", + "closes_at": "2026-01-08T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000", + "max_leverage": "2", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "demo-equity-acme", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_adverse_touch_v1", + "configuration": { + "version": "1", + "participation_bps": 5000, + "fee_schedules": [ + { + "schedule_id": "demo-acme-fees-v1", + "instrument_id": "demo-equity-acme", + "settlement_currency": "USD", + "minimum": "0.3", + "maximum": "1", + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "0.25", + "rounding": "up", + "applies_to": "any" + }, + { + "name": "exchange", + "currency": "USD", + "kind": "notional_bps", + "value": 10, + "rounding": "up", + "applies_to": "taker" + }, + { + "name": "maker_rebate", + "currency": "USD", + "kind": "notional_bps", + "value": -2, + "rounding": "nearest", + "applies_to": "maker" + } + ] + } + ], + "spread_model": { + "model": "fixed_half_spread_v1", + "half_spread_bps": 5 + }, + "impact_model": { + "model": "linear_participation_v1", + "coefficient_bps": 25, + "missing_volume_policy": "reject" + } + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "target_weights", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "weight": "0.1" + } + ] + }, + { + "type": "emit_metric", + "name": "desired_weight", + "value": "0.1" + } + ] + }, + { + "after_slice_sequence": "3", + "intents": [ + { + "type": "target_quantities", + "targets": [ + { + "instrument_id": "demo-equity-acme", + "quantity": "2.5" + } + ] + } + ] + } + ], + "slices": [ + { + "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": "demo-equity-acme", + "open": "100", + "high": "105", + "low": "99", + "close": "104", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "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": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-01-05T14:30:00Z", + "end_at": "2026-01-05T21:00:00Z", + "available_at": "2026-01-05T21:00:01Z", + "received_at": "2026-01-05T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "103", + "high": "108", + "low": "102", + "close": "107", + "volume": "13" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-05T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-05T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "3", + "start_at": "2026-01-06T14:30:00Z", + "end_at": "2026-01-06T21:00:00Z", + "available_at": "2026-01-06T21:00:01Z", + "received_at": "2026-01-06T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "107", + "high": "109", + "low": "104", + "close": "105", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-06T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-06T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "4", + "start_at": "2026-01-07T14:30:00Z", + "end_at": "2026-01-07T21:00:00Z", + "available_at": "2026-01-07T21:00:01Z", + "received_at": "2026-01-07T21:00:02Z", + "bars": [ + { + "instrument_id": "demo-equity-acme", + "open": "105", + "high": "107", + "low": "103", + "close": "106", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "demo-equity-acme", + "effective_at": "2026-01-07T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-01-07T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "demo-equity-acme", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/demo.scenario.jsonl b/contracts/v15/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..251ded8 --- /dev/null +++ b/contracts/v15/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine-demo","purpose":"deterministic conformance fixture"},"run_id":"demo","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"10000"}],"positions":[{"instrument_id":"demo-equity-acme","quantity":"1","cost_basis":"90","realized_pnl":"5","dividend_pnl":"1","execution_fees":"0.5","borrow_fees":"0.25"}],"marks":[{"instrument_id":"demo-equity-acme","price":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"demo-equity-acme","symbol":"ACME","quote_currency":"USD","tick_size":"0.01","lot_size":"0.001"}],"venue_calendars":[{"calendar_id":"demo-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["demo-equity-acme"],"sessions":[{"session_date":"2026-01-01","policy":"holiday","phases":[]},{"session_date":"2026-01-02","policy":"regular","phases":[{"phase":"premarket","opens_at":"2026-01-02T09:00:00Z","closes_at":"2026-01-02T14:25:00Z"},{"phase":"opening_auction","opens_at":"2026-01-02T14:25:00Z","closes_at":"2026-01-02T14:30:00Z"},{"phase":"regular","opens_at":"2026-01-02T14:30:00Z","closes_at":"2026-01-02T20:55:00Z"},{"phase":"closing_auction","opens_at":"2026-01-02T20:55:00Z","closes_at":"2026-01-02T21:00:00Z"},{"phase":"postmarket","opens_at":"2026-01-02T21:00:00Z","closes_at":"2026-01-03T01:00:00Z"}]},{"session_date":"2026-01-05","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-05T14:30:00Z","closes_at":"2026-01-05T21:00:00Z"}]},{"session_date":"2026-01-06","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-06T14:30:00Z","closes_at":"2026-01-06T21:00:00Z"}]},{"session_date":"2026-01-07","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-01-07T14:30:00Z","closes_at":"2026-01-07T21:00:00Z"}]},{"session_date":"2026-01-08","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-01-08T14:30:00Z","closes_at":"2026-01-08T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000","max_leverage":"2","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"demo-equity-acme","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"completed_bar_adverse_touch_v1","configuration":{"version":"1","participation_bps":5000,"fee_schedules":[{"schedule_id":"demo-acme-fees-v1","instrument_id":"demo-equity-acme","settlement_currency":"USD","minimum":"0.3","maximum":"1","components":[{"name":"broker","currency":"USD","kind":"fixed","value":"0.25","rounding":"up","applies_to":"any"},{"name":"exchange","currency":"USD","kind":"notional_bps","value":10,"rounding":"up","applies_to":"taker"},{"name":"maker_rebate","currency":"USD","kind":"notional_bps","value":-2,"rounding":"nearest","applies_to":"maker"}]}],"spread_model":{"model":"fixed_half_spread_v1","half_spread_bps":5},"impact_model":{"model":"linear_participation_v1","coefficient_bps":25,"missing_volume_policy":"reject"}}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"demo-equity-acme","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","weight":"0.1"}],"type":"target_weights"},{"name":"desired_weight","type":"emit_metric","value":"0.1"}],"market_slice":{"available_at":"2026-01-02T21:00:01Z","bars":[{"close":"104","high":"105","instrument_id":"demo-equity-acme","low":"99","open":"100","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-02T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-02T21:00:02Z","slice_sequence":"1","start_at":"2026-01-02T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","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":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-05T21:00:01Z","bars":[{"close":"107","high":"108","instrument_id":"demo-equity-acme","low":"102","open":"103","volume":"13"}],"corporate_actions":[],"end_at":"2026-01-05T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-05T21:00:02Z","slice_sequence":"2","start_at":"2026-01-05T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-05T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-05T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"15","payload":{"intents":[{"targets":[{"instrument_id":"demo-equity-acme","quantity":"2.5"}],"type":"target_quantities"}],"market_slice":{"available_at":"2026-01-06T21:00:01Z","bars":[{"close":"105","high":"109","instrument_id":"demo-equity-acme","low":"104","open":"107","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-06T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-06T21:00:02Z","slice_sequence":"3","start_at":"2026-01-06T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-06T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-06T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"15","payload":{"intents":[],"market_slice":{"available_at":"2026-01-07T21:00:01Z","bars":[{"close":"106","high":"107","instrument_id":"demo-equity-acme","low":"103","open":"105","volume":"100"}],"corporate_actions":[],"end_at":"2026-01-07T21:00:00Z","fx_rates":[{"currency":"USD","rate":"1"}],"received_at":"2026-01-07T21:00:02Z","slice_sequence":"4","start_at":"2026-01-07T14:30:00Z","borrow_observations":[{"instrument_id":"demo-equity-acme","effective_at":"2026-01-07T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-01-07T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"15","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v15/fixtures/fill-clipped.journal.jsonl b/contracts/v15/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..133c789 --- /dev/null +++ b/contracts/v15/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"fill-clipped-event-000000000001","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"02138360b4f18f05c851efd2c16fecc1adb49ae668f6f7e72c3d903bea561002","execution_model":"completed_bar_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"fill-clipped-event-000000000002","causation_ids":["fill-clipped-event-000000000001"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"550"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"fill-clipped-event-000000000003","causation_ids":["fill-clipped-event-000000000002"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"550","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550","fx_rate":"1","base_value":"550","interest":"0","base_interest":"0","settled_amount":"550","unsettled_amount":"0","base_settled_value":"550","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"550","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550","maintenance_excess":"550","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"fill-clipped-event-000000000004","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"5","event_id":"fill-clipped-event-000000000005","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.004081"}} +{"contract_version":"15","engine_sequence":"6","event_id":"fill-clipped-event-000000000006","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"7","event_id":"fill-clipped-event-000000000007","causation_ids":["fill-clipped-event-000000000004"],"run_id":"fill-clipped","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.004081","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.004081","unrealized_pnl":"0","equity":"550.004081","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.004081","fx_rate":"1","base_value":"550.004081","interest":"0.004081","base_interest":"0.004081","settled_amount":"550.004081","unsettled_amount":"0","base_settled_value":"550.004081","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.004081","settled_cash":"550.004081","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.004081","maintenance_excess":"550.004081","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"8","event_id":"fill-clipped-event-000000000008","causation_ids":[],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"9","event_id":"fill-clipped-event-000000000009","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"550.004081","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.004081","closing_balance":"550.008162"}} +{"contract_version":"15","engine_sequence":"10","event_id":"fill-clipped-event-000000000010","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_clipped","payload":{"reason":{"version":"1","policy":"max_leverage","threshold":{"unit":"ratio","value":"1"}},"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","proposed_quantity":"10","permitted_quantity":"0","price":"100"}} +{"contract_version":"15","engine_sequence":"11","event_id":"fill-clipped-event-000000000011","causation_ids":["fill-clipped-event-000000000006","fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"order_cancelled","payload":{"order":{"order_id":"fill-clipped-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"market","trigger_price":null,"limit_price":null,"time_in_force":"ioc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"fill-clipped-event-000000000006","updated_event_id":"fill-clipped-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"cancelled","rejection_reason":null},"reason":"market_ioc"}} +{"contract_version":"15","engine_sequence":"12","event_id":"fill-clipped-event-000000000012","causation_ids":["fill-clipped-event-000000000008"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"13","event_id":"fill-clipped-event-000000000013","causation_ids":["fill-clipped-event-000000000012"],"run_id":"fill-clipped","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"02138360b4f18f05c851efd2c16fecc1adb49ae668f6f7e72c3d903bea561002","execution_model":"completed_bar_v1","valuation":{"base_currency":"USD","cash":"550.008162","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.008162","unrealized_pnl":"0","equity":"550.008162","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"550.008162","fx_rate":"1","base_value":"550.008162","interest":"0.008162","base_interest":"0.008162","settled_amount":"550.008162","unsettled_amount":"0","base_settled_value":"550.008162","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"100","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.008162","settled_cash":"550.008162","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"550.008162","maintenance_excess":"550.008162","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":0,"rejected":0,"cancelled":1}}} diff --git a/contracts/v15/fixtures/fill-clipped.scenario.json b/contracts/v15/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..319e246 --- /dev/null +++ b/contracts/v15/fixtures/fill-clipped.scenario.json @@ -0,0 +1,273 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine", + "purpose": "fill clipping conformance fixture" + }, + "run_id": "fill-clipped", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "550" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "completed_bar_v1", + "configuration": { + "version": "2", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "market", + "trigger_price": null, + "limit_price": null, + "time_in_force": "ioc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/order-book.journal.jsonl b/contracts/v15/fixtures/order-book.journal.jsonl new file mode 100644 index 0000000..7c9fded --- /dev/null +++ b/contracts/v15/fixtures/order-book.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"order-book-event-000000000001","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"f330f2a12a85e8b24bd3bc1bbb7fdf9a08654ed0d7dd8f0a3c712e031ef80128","execution_model":"order_book_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"order-book-event-000000000002","causation_ids":["order-book-event-000000000001"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"order-book-event-000000000003","causation_ids":["order-book-event-000000000002"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"order-book-event-000000000004","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00.000000Z","available_at":"2026-02-02T14:31:01.000000Z","received_at":"2026-02-02T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}} +{"contract_version":"15","engine_sequence":"5","event_id":"order-book-event-000000000005","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"15","engine_sequence":"6","event_id":"order-book-event-000000000006","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"order-book-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"order-book-event-000000000006","updated_event_id":"order-book-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"7","event_id":"order-book-event-000000000007","causation_ids":["order-book-event-000000000004"],"run_id":"order-book","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"8","event_id":"order-book-event-000000000008","causation_ids":[],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00.000000Z","available_at":"2026-02-03T14:35:01.000000Z","received_at":"2026-02-03T14:35:02.000000Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00.000000Z","available_at":"2026-02-03T14:36:01.000000Z","received_at":"2026-02-03T14:36:02.000000Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}} +{"contract_version":"15","engine_sequence":"9","event_id":"order-book-event-000000000009","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"15","engine_sequence":"10","event_id":"order-book-event-000000000010","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000001","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"100","notional":"400","fee":"10","executed_at":"2026-02-03T14:35:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"11","event_id":"order-book-event-000000000011","causation_ids":["order-book-event-000000000006","order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"order-book-fill-000000000002","order_id":"order-book-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:36:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"12","event_id":"order-book-event-000000000012","causation_ids":["order-book-event-000000000008"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"13","event_id":"order-book-event-000000000013","causation_ids":["order-book-event-000000000012"],"run_id":"order-book","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"f330f2a12a85e8b24bd3bc1bbb7fdf9a08654ed0d7dd8f0a3c712e031ef80128","execution_model":"order_book_v1","valuation":{"base_currency":"USD","cash":"980.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1020","realized_pnl":"0.02968","unrealized_pnl":"-20","equity":"1980.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"980.02968","fx_rate":"1","base_value":"980.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"980.02968","unsettled_amount":"0","base_settled_value":"980.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1020","base_cost_basis":"1020","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-20","base_unrealized_pnl":"-20","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"980.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1480.02968","maintenance_excess":"1730.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/order-book.scenario.json b/contracts/v15/fixtures/order-book.scenario.json new file mode 100644 index 0000000..7e27ab1 --- /dev/null +++ b/contracts/v15/fixtures/order-book.scenario.json @@ -0,0 +1,378 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded order-book replay conformance fixture" + }, + "run_id": "order-book", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "order_book_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ], + "max_depth_levels": 10 + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [ + { + "type": "snapshot", + "instrument_id": "clip-equity", + "event_at": "2026-02-02T14:31:00Z", + "available_at": "2026-02-02T14:31:01Z", + "received_at": "2026-02-02T14:31:02Z", + "ingest_sequence": "1", + "book_sequence": "1", + "bids": [ + { + "price": "49", + "quantity": "20" + } + ], + "asks": [ + { + "price": "51", + "quantity": "20" + } + ] + } + ] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [ + { + "type": "snapshot", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "book_sequence": "1", + "bids": [ + { + "price": "100", + "quantity": "5" + } + ], + "asks": [ + { + "price": "101", + "quantity": "20" + } + ] + }, + { + "type": "set", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "book_sequence": "2", + "side": "bid", + "price": "100", + "quantity": "3" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "book_sequence": "3", + "price": "100", + "quantity": "3", + "aggressor_side": "sell" + }, + { + "type": "set", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "book_sequence": "4", + "side": "bid", + "price": "100", + "quantity": "10" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:35:00Z", + "available_at": "2026-02-03T14:35:01Z", + "received_at": "2026-02-03T14:35:02Z", + "ingest_sequence": "5", + "book_sequence": "5", + "price": "100", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:36:00Z", + "available_at": "2026-02-03T14:36:01Z", + "received_at": "2026-02-03T14:36:02Z", + "ingest_sequence": "6", + "book_sequence": "6", + "price": "100", + "quantity": "6", + "aggressor_side": "sell" + } + ] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/order-book.scenario.jsonl b/contracts/v15/fixtures/order-book.scenario.jsonl new file mode 100644 index 0000000..ef9c90e --- /dev/null +++ b/contracts/v15/fixtures/order-book.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded order-book replay conformance fixture"},"run_id":"order-book","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"order_book_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}],"max_depth_levels":10}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","scenario_sequence":"2","record_type":"market_slice","payload":{"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}],"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-02T14:31:00Z","available_at":"2026-02-02T14:31:01Z","received_at":"2026-02-02T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"49","quantity":"20"}],"asks":[{"price":"51","quantity":"20"}]}]}}} +{"contract_version":"15","scenario_sequence":"3","record_type":"market_slice","payload":{"intents":[],"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[{"type":"snapshot","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","book_sequence":"1","bids":[{"price":"100","quantity":"5"}],"asks":[{"price":"101","quantity":"20"}]},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","book_sequence":"2","side":"bid","price":"100","quantity":"3"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","book_sequence":"3","price":"100","quantity":"3","aggressor_side":"sell"},{"type":"set","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","book_sequence":"4","side":"bid","price":"100","quantity":"10"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:35:00Z","available_at":"2026-02-03T14:35:01Z","received_at":"2026-02-03T14:35:02Z","ingest_sequence":"5","book_sequence":"5","price":"100","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:36:00Z","available_at":"2026-02-03T14:36:01Z","received_at":"2026-02-03T14:36:02Z","ingest_sequence":"6","book_sequence":"6","price":"100","quantity":"6","aggressor_side":"sell"}]}}} +{"contract_version":"15","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v15/fixtures/quote-trade.journal.jsonl b/contracts/v15/fixtures/quote-trade.journal.jsonl new file mode 100644 index 0000000..20bc174 --- /dev/null +++ b/contracts/v15/fixtures/quote-trade.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"15","engine_sequence":"1","event_id":"quote-trade-event-000000000001","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"run_started","payload":{"scenario_sha256":"b062a14acf0722c6d77e0e81985e12bf3a2a150dd7e493e1a811a393032c8d7f","execution_model":"quote_trade_v1"}} +{"contract_version":"15","engine_sequence":"2","event_id":"quote-trade-event-000000000002","causation_ids":["quote-trade-event-000000000001"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"initial_state","payload":{"portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"valuation":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}}} +{"contract_version":"15","engine_sequence":"3","event_id":"quote-trade-event-000000000003","causation_ids":["quote-trade-event-000000000002"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0","unrealized_pnl":"0","equity":"2000","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000","fx_rate":"1","base_value":"2000","interest":"0","base_interest":"0","settled_amount":"2000","unsettled_amount":"0","base_settled_value":"2000","base_unsettled_value":"0"}],"positions":[],"execution_fee_components":[],"cash_interest":"0","settled_cash":"2000","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000","maintenance_excess":"2000","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"4","event_id":"quote-trade-event-000000000004","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00.000000Z","end_at":"2026-02-02T21:00:00.000000Z","available_at":"2026-02-02T21:00:01.000000Z","received_at":"2026-02-02T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"5","event_id":"quote-trade-event-000000000005","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-02T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-02T14:30:00.000000Z","period_end":"2026-02-02T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.01484"}} +{"contract_version":"15","engine_sequence":"6","event_id":"quote-trade-event-000000000006","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"order_accepted","payload":{"order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null,"origin":"direct","created_event_id":"quote-trade-event-000000000006","updated_event_id":"quote-trade-event-000000000006","created_sequence":"6","created_at":"2026-02-02T21:00:02.000000Z","eligible_after_slice_sequence":"1","triggered_at":null,"triggered_slice_sequence":null,"filled_quantity":"0","filled_notional":"0","status":"working","rejection_reason":null}} +{"contract_version":"15","engine_sequence":"7","event_id":"quote-trade-event-000000000007","causation_ids":["quote-trade-event-000000000004"],"run_id":"quote-trade","recorded_at":"2026-02-02T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"2000.01484","net_market_value":"0","long_market_value":"0","short_market_value":"0","gross_exposure":"0","cost_basis":"0","realized_pnl":"0.01484","unrealized_pnl":"0","equity":"2000.01484","dividend_pnl":"0","execution_fees":"0","borrow_fees":"0","total_fees":"0","cash_balances":[{"currency":"USD","amount":"2000.01484","fx_rate":"1","base_value":"2000.01484","interest":"0.01484","base_interest":"0.01484","settled_amount":"2000.01484","unsettled_amount":"0","base_settled_value":"2000.01484","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"0","mark":"50","fx_rate":"1","market_value":"0","base_market_value":"0","cost_basis":"0","base_cost_basis":"0","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"0","base_unrealized_pnl":"0","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"0","base_execution_fees":"0","borrow_fees":"0","base_borrow_fees":"0","total_fees":"0","base_total_fees":"0","execution_fee_components":[],"settled_quantity":"0","unsettled_quantity":"0"}],"execution_fee_components":[],"cash_interest":"0.01484","settled_cash":"2000.01484","unsettled_cash":"0","margin":{"initial_requirement":"0","maintenance_requirement":"0","initial_excess":"2000.01484","maintenance_excess":"2000.01484","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"8","event_id":"quote-trade-event-000000000008","causation_ids":[],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"market_slice_received","payload":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00.000000Z","end_at":"2026-02-03T21:00:00.000000Z","available_at":"2026-02-03T21:00:01.000000Z","received_at":"2026-02-03T21:00:02.000000Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00.000000Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00.000000Z","available_at":"2026-02-03T14:31:01.000000Z","received_at":"2026-02-03T14:31:02.000000Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00.000000Z","available_at":"2026-02-03T14:32:01.000000Z","received_at":"2026-02-03T14:32:02.000000Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00.000000Z","available_at":"2026-02-03T14:33:01.000000Z","received_at":"2026-02-03T14:33:02.000000Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00.000000Z","available_at":"2026-02-03T14:34:01.000000Z","received_at":"2026-02-03T14:34:02.000000Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]}} +{"contract_version":"15","engine_sequence":"9","event_id":"quote-trade-event-000000000009","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"cash_interest_applied","payload":{"observation":{"currency":"USD","effective_at":"2026-02-03T14:30:00.000000Z","credit_rate_bps":100,"debit_rate_bps":200},"opening_balance":"2000.01484","applied_rate_bps":100,"day_count":"actual_365","compounding":"simple","period_start":"2026-02-03T14:30:00.000000Z","period_end":"2026-02-03T21:00:00.000000Z","amount":"0.01484","closing_balance":"2000.02968"}} +{"contract_version":"15","engine_sequence":"10","event_id":"quote-trade-event-000000000010","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000001","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"4","price":"99","notional":"396","fee":"10","executed_at":"2026-02-03T14:33:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"11","event_id":"quote-trade-event-000000000011","causation_ids":["quote-trade-event-000000000006","quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"fill_applied","payload":{"fill_id":"quote-trade-fill-000000000002","order_id":"quote-trade-order-000000000001","instrument_id":"clip-equity","quote_currency":"USD","side":"buy","quantity":"6","price":"100","notional":"600","fee":"10","executed_at":"2026-02-03T14:34:00.000000Z","slice_sequence":"2","fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"10","quote_amount":"10"}]}} +{"contract_version":"15","engine_sequence":"12","event_id":"quote-trade-event-000000000012","causation_ids":["quote-trade-event-000000000008"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"valuation","payload":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]}} +{"contract_version":"15","engine_sequence":"13","event_id":"quote-trade-event-000000000013","causation_ids":["quote-trade-event-000000000012"],"run_id":"quote-trade","recorded_at":"2026-02-03T21:00:02.000000Z","event_type":"run_completed","payload":{"scenario_sha256":"b062a14acf0722c6d77e0e81985e12bf3a2a150dd7e493e1a811a393032c8d7f","execution_model":"quote_trade_v1","valuation":{"base_currency":"USD","cash":"984.02968","net_market_value":"1000","long_market_value":"1000","short_market_value":"0","gross_exposure":"1000","cost_basis":"1016","realized_pnl":"0.02968","unrealized_pnl":"-16","equity":"1984.02968","dividend_pnl":"0","execution_fees":"20","borrow_fees":"0","total_fees":"20","cash_balances":[{"currency":"USD","amount":"984.02968","fx_rate":"1","base_value":"984.02968","interest":"0.02968","base_interest":"0.02968","settled_amount":"984.02968","unsettled_amount":"0","base_settled_value":"984.02968","base_unsettled_value":"0"}],"positions":[{"instrument_id":"clip-equity","quote_currency":"USD","quantity":"10","mark":"100","fx_rate":"1","market_value":"1000","base_market_value":"1000","cost_basis":"1016","base_cost_basis":"1016","realized_pnl":"0","base_realized_pnl":"0","unrealized_pnl":"-16","base_unrealized_pnl":"-16","dividend_pnl":"0","base_dividend_pnl":"0","execution_fees":"20","base_execution_fees":"20","borrow_fees":"0","base_borrow_fees":"0","total_fees":"20","base_total_fees":"20","execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"settled_quantity":"10","unsettled_quantity":"0"}],"execution_fee_components":[{"name":"broker","kind":"fixed","currency":"USD","amount":"20","quote_currency":"USD","quote_amount":"20","base_amount":"20"}],"cash_interest":"0.02968","settled_cash":"984.02968","unsettled_cash":"0","margin":{"initial_requirement":"500","maintenance_requirement":"250","initial_excess":"1484.02968","maintenance_excess":"1734.02968","margin_call":false},"group_exposures":[]},"order_counts":{"total":1,"active":0,"filled":1,"rejected":0,"cancelled":0}}} diff --git a/contracts/v15/fixtures/quote-trade.scenario.json b/contracts/v15/fixtures/quote-trade.scenario.json new file mode 100644 index 0000000..6eb58b8 --- /dev/null +++ b/contracts/v15/fixtures/quote-trade.scenario.json @@ -0,0 +1,319 @@ +{ + "contract_version": "15", + "metadata": { + "producer": "trading-engine", + "purpose": "bounded quote and trade replay fixture" + }, + "run_id": "quote-trade", + "base_currency": "USD", + "initial_portfolio": { + "cash": [ + { + "currency": "USD", + "amount": "2000" + } + ], + "positions": [], + "marks": [], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ] + }, + "instruments": [ + { + "instrument_id": "clip-equity", + "symbol": "CLIP", + "quote_currency": "USD", + "tick_size": "0.01", + "lot_size": "1" + } + ], + "venue_calendars": [ + { + "calendar_id": "clip-xnas-2026", + "calendar_version": "1", + "venue_id": "XNAS", + "instrument_ids": [ + "clip-equity" + ], + "sessions": [ + { + "session_date": "2026-02-02", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-02T14:30:00Z", + "closes_at": "2026-02-02T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-03", + "policy": "regular", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-03T14:30:00Z", + "closes_at": "2026-02-03T21:00:00Z" + } + ] + }, + { + "session_date": "2026-02-04", + "policy": "early_close", + "phases": [ + { + "phase": "regular", + "opens_at": "2026-02-04T14:30:00Z", + "closes_at": "2026-02-04T18:00:00Z" + } + ] + } + ] + } + ], + "risk": { + "max_gross_exposure": "1000000000", + "max_leverage": "1", + "short_borrow_bps": 100, + "instrument_policies": [ + { + "instrument_id": "clip-equity", + "max_order_quantity": "1000", + "max_long_position": "1000", + "max_short_position": "1000", + "max_notional_exposure": "1000000000", + "initial_margin_bps": 5000, + "maintenance_margin_bps": 2500, + "shorting_allowed": true + } + ], + "groups": [] + }, + "execution": { + "model": "quote_trade_v1", + "configuration": { + "version": "1", + "participation_bps": 10000, + "fee_schedules": [ + { + "schedule_id": "clip-fees-v1", + "instrument_id": "clip-equity", + "settlement_currency": "USD", + "minimum": null, + "maximum": null, + "components": [ + { + "name": "broker", + "currency": "USD", + "kind": "fixed", + "value": "10", + "rounding": "up", + "applies_to": "any" + } + ] + } + ] + } + }, + "max_internal_events": 1000, + "schedule": [ + { + "after_slice_sequence": "1", + "intents": [ + { + "type": "submit_order", + "instrument_id": "clip-equity", + "side": "buy", + "quantity": "10", + "order_kind": "limit", + "trigger_price": null, + "limit_price": "100", + "time_in_force": "gtc", + "venue_id": null, + "calendar_id": null, + "expires_at": null + } + ] + } + ], + "slices": [ + { + "slice_sequence": "1", + "start_at": "2026-02-02T14:30:00Z", + "end_at": "2026-02-02T21:00:00Z", + "available_at": "2026-02-02T21:00:01Z", + "received_at": "2026-02-02T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "50", + "high": "50", + "low": "50", + "close": "50", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-02T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-02T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [], + "order_book_events": [] + }, + { + "slice_sequence": "2", + "start_at": "2026-02-03T14:30:00Z", + "end_at": "2026-02-03T21:00:00Z", + "available_at": "2026-02-03T21:00:01Z", + "received_at": "2026-02-03T21:00:02Z", + "bars": [ + { + "instrument_id": "clip-equity", + "open": "100", + "high": "100", + "low": "100", + "close": "100", + "volume": "100" + } + ], + "fx_rates": [ + { + "currency": "USD", + "rate": "1" + } + ], + "corporate_actions": [], + "borrow_observations": [ + { + "instrument_id": "clip-equity", + "effective_at": "2026-02-03T14:30:00Z", + "available_quantity": "1000", + "annual_rate_bps": 100, + "recalled": false + } + ], + "cash_rate_observations": [ + { + "currency": "USD", + "effective_at": "2026-02-03T14:30:00Z", + "credit_rate_bps": 100, + "debit_rate_bps": 200 + } + ], + "settlement_failures": [], + "lifecycle_events": [], + "market_events": [ + { + "type": "quote", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:31:00Z", + "available_at": "2026-02-03T14:31:01Z", + "received_at": "2026-02-03T14:31:02Z", + "ingest_sequence": "1", + "bid_price": "99", + "bid_quantity": "20", + "ask_price": "101", + "ask_quantity": "20" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:32:00Z", + "available_at": "2026-02-03T14:32:01Z", + "received_at": "2026-02-03T14:32:02Z", + "ingest_sequence": "2", + "price": "100", + "quantity": "5", + "aggressor_side": "unknown" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:33:00Z", + "available_at": "2026-02-03T14:33:01Z", + "received_at": "2026-02-03T14:33:02Z", + "ingest_sequence": "3", + "price": "99", + "quantity": "4", + "aggressor_side": "sell" + }, + { + "type": "trade", + "instrument_id": "clip-equity", + "event_at": "2026-02-03T14:34:00Z", + "available_at": "2026-02-03T14:34:01Z", + "received_at": "2026-02-03T14:34:02Z", + "ingest_sequence": "4", + "price": "100", + "quantity": "10", + "aggressor_side": "sell" + } + ], + "order_book_events": [] + } + ], + "financing": { + "day_count": "actual_365", + "compounding": "simple", + "borrow_missing_data": "reject", + "cash_missing_data": "reject", + "locate_policy": "clip_fill", + "recall_policy": "close_out" + }, + "settlement": { + "cash_buying_power": "total_cash", + "position_availability": "total_positions", + "calendars": [ + { + "calendar_id": "default-settlement", + "version": "1", + "business_dates": [ + "2026-01-02", + "2026-01-05", + "2026-01-06", + "2026-01-07", + "2026-01-08", + "2026-01-09", + "2026-02-02", + "2026-02-03", + "2026-02-04", + "2026-02-05" + ] + } + ], + "rules": [ + { + "instrument_id": "clip-equity", + "calendar_id": "default-settlement", + "lag_business_days": 1 + } + ] + } +} diff --git a/contracts/v15/fixtures/quote-trade.scenario.jsonl b/contracts/v15/fixtures/quote-trade.scenario.jsonl new file mode 100644 index 0000000..6c2b50d --- /dev/null +++ b/contracts/v15/fixtures/quote-trade.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"15","scenario_sequence":"1","record_type":"scenario_header","payload":{"metadata":{"producer":"trading-engine","purpose":"bounded quote and trade replay fixture"},"run_id":"quote-trade","base_currency":"USD","initial_portfolio":{"cash":[{"currency":"USD","amount":"2000"}],"positions":[],"marks":[],"fx_rates":[{"currency":"USD","rate":"1"}]},"instruments":[{"instrument_id":"clip-equity","symbol":"CLIP","quote_currency":"USD","tick_size":"0.01","lot_size":"1"}],"venue_calendars":[{"calendar_id":"clip-xnas-2026","calendar_version":"1","venue_id":"XNAS","instrument_ids":["clip-equity"],"sessions":[{"session_date":"2026-02-02","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-02T14:30:00Z","closes_at":"2026-02-02T21:00:00Z"}]},{"session_date":"2026-02-03","policy":"regular","phases":[{"phase":"regular","opens_at":"2026-02-03T14:30:00Z","closes_at":"2026-02-03T21:00:00Z"}]},{"session_date":"2026-02-04","policy":"early_close","phases":[{"phase":"regular","opens_at":"2026-02-04T14:30:00Z","closes_at":"2026-02-04T18:00:00Z"}]}]}],"risk":{"max_gross_exposure":"1000000000","max_leverage":"1","short_borrow_bps":100,"instrument_policies":[{"instrument_id":"clip-equity","max_order_quantity":"1000","max_long_position":"1000","max_short_position":"1000","max_notional_exposure":"1000000000","initial_margin_bps":5000,"maintenance_margin_bps":2500,"shorting_allowed":true}],"groups":[]},"execution":{"model":"quote_trade_v1","configuration":{"version":"1","participation_bps":10000,"fee_schedules":[{"schedule_id":"clip-fees-v1","instrument_id":"clip-equity","settlement_currency":"USD","minimum":null,"maximum":null,"components":[{"name":"broker","currency":"USD","kind":"fixed","value":"10","rounding":"up","applies_to":"any"}]}]}},"max_internal_events":1000,"financing":{"day_count":"actual_365","compounding":"simple","borrow_missing_data":"reject","cash_missing_data":"reject","locate_policy":"clip_fill","recall_policy":"close_out"},"settlement":{"cash_buying_power":"total_cash","position_availability":"total_positions","calendars":[{"calendar_id":"default-settlement","version":"1","business_dates":["2026-01-02","2026-01-05","2026-01-06","2026-01-07","2026-01-08","2026-01-09","2026-02-02","2026-02-03","2026-02-04","2026-02-05"]}],"rules":[{"instrument_id":"clip-equity","calendar_id":"default-settlement","lag_business_days":1}]}}} +{"contract_version":"15","scenario_sequence":"2","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"1","start_at":"2026-02-02T14:30:00Z","end_at":"2026-02-02T21:00:00Z","available_at":"2026-02-02T21:00:01Z","received_at":"2026-02-02T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"50","high":"50","low":"50","close":"50","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-02T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-02T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[],"order_book_events":[]},"intents":[{"type":"submit_order","instrument_id":"clip-equity","side":"buy","quantity":"10","order_kind":"limit","trigger_price":null,"limit_price":"100","time_in_force":"gtc","venue_id":null,"calendar_id":null,"expires_at":null}]}} +{"contract_version":"15","scenario_sequence":"3","record_type":"market_slice","payload":{"market_slice":{"slice_sequence":"2","start_at":"2026-02-03T14:30:00Z","end_at":"2026-02-03T21:00:00Z","available_at":"2026-02-03T21:00:01Z","received_at":"2026-02-03T21:00:02Z","bars":[{"instrument_id":"clip-equity","open":"100","high":"100","low":"100","close":"100","volume":"100"}],"fx_rates":[{"currency":"USD","rate":"1"}],"corporate_actions":[],"borrow_observations":[{"instrument_id":"clip-equity","effective_at":"2026-02-03T14:30:00Z","available_quantity":"1000","annual_rate_bps":100,"recalled":false}],"cash_rate_observations":[{"currency":"USD","effective_at":"2026-02-03T14:30:00Z","credit_rate_bps":100,"debit_rate_bps":200}],"settlement_failures":[],"lifecycle_events":[],"market_events":[{"type":"quote","instrument_id":"clip-equity","event_at":"2026-02-03T14:31:00Z","available_at":"2026-02-03T14:31:01Z","received_at":"2026-02-03T14:31:02Z","ingest_sequence":"1","bid_price":"99","bid_quantity":"20","ask_price":"101","ask_quantity":"20"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:32:00Z","available_at":"2026-02-03T14:32:01Z","received_at":"2026-02-03T14:32:02Z","ingest_sequence":"2","price":"100","quantity":"5","aggressor_side":"unknown"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:33:00Z","available_at":"2026-02-03T14:33:01Z","received_at":"2026-02-03T14:33:02Z","ingest_sequence":"3","price":"99","quantity":"4","aggressor_side":"sell"},{"type":"trade","instrument_id":"clip-equity","event_at":"2026-02-03T14:34:00Z","available_at":"2026-02-03T14:34:01Z","received_at":"2026-02-03T14:34:02Z","ingest_sequence":"4","price":"100","quantity":"10","aggressor_side":"sell"}],"order_book_events":[]},"intents":[]}} +{"contract_version":"15","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v15/journal.schema.json b/contracts/v15/journal.schema.json new file mode 100644 index 0000000..d9360c0 --- /dev/null +++ b/contracts/v15/journal.schema.json @@ -0,0 +1,2427 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v15/journal.schema.json", + "title": "Trading Engine v15 audit journal record", + "type": "object", + "additionalProperties": false, + "required": [ + "contract_version", + "engine_sequence", + "event_id", + "causation_ids", + "run_id", + "recorded_at", + "event_type", + "payload" + ], + "properties": { + "contract_version": { + "const": "15" + }, + "engine_sequence": { + "$ref": "#/$defs/sequence" + }, + "event_id": { + "$ref": "#/$defs/identifier" + }, + "causation_ids": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/identifier" + } + }, + "run_id": { + "$ref": "#/$defs/identifier" + }, + "recorded_at": { + "$ref": "#/$defs/timestamp" + }, + "event_type": { + "enum": [ + "run_started", + "initial_state", + "market_slice_received", + "target_portfolio_requested", + "order_accepted", + "order_rejected", + "order_triggered", + "order_cancelled", + "split_applied", + "cash_dividend_applied", + "distribution_applied", + "lifecycle_applied", + "order_adjusted", + "execution_price_selected", + "fill_applied", + "settlement_instruction_created", + "settlement_completed", + "settlement_failed", + "fill_clipped", + "borrow_fee_applied", + "borrow_charge_applied", + "borrow_recall_received", + "cash_interest_applied", + "margin_call", + "margin_restored", + "intent_rejected", + "metric_emitted", + "valuation", + "run_completed" + ] + }, + "payload": { + "type": "object" + } + }, + "allOf": [ + { + "if": { "properties": { "event_type": { "const": "execution_price_selected" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/executionPriceSelected" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_started" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runStarted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "initial_state" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/initialState" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "market_slice_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/marketSlice" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "target_portfolio_requested" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/targetPortfolio" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "order_accepted", + "order_rejected", + "order_triggered" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/order" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_cancelled" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderCancelled" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "split_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/splitApplied" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_dividend_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/dividendApplied" + } + } + } + }, + { + "if": { "properties": { "event_type": { "const": "distribution_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/distributionApplied" } } } + }, + { + "if": { "properties": { "event_type": { "const": "lifecycle_applied" } } }, + "then": { "properties": { "payload": { "$ref": "#/$defs/lifecycleApplied" } } } + }, + { + "if": { + "properties": { + "event_type": { + "const": "order_adjusted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/orderAdjusted" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fill" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "settlement_instruction_created", + "settlement_completed", + "settlement_failed" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/settlementInstruction" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "fill_clipped" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/fillClipped" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_fee_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowFee" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_charge_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowCharge" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "borrow_recall_received" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/borrowRecall" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "cash_interest_applied" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/cashInterest" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "enum": [ + "margin_call", + "margin_restored", + "valuation" + ] + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/valuation" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "intent_rejected" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/intentRejected" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "metric_emitted" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/metric" + } + } + } + }, + { + "if": { + "properties": { + "event_type": { + "const": "run_completed" + } + } + }, + "then": { + "properties": { + "payload": { + "$ref": "#/$defs/runCompleted" + } + } + } + } + ], + "$defs": { + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { + "type": "string", + "pattern": "^[1-9][0-9]*$" + }, + "nonnegativeSequence": { + "type": "string", + "pattern": "^(?:0|[1-9][0-9]*)$" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "runStarted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" + }, + "valuation": { + "$ref": "#/$defs/valuation" + } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "open", + "high", + "low", + "close", + "volume" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "open": { + "$ref": "#/$defs/positiveDecimal" + }, + "high": { + "$ref": "#/$defs/positiveDecimal" + }, + "low": { + "$ref": "#/$defs/positiveDecimal" + }, + "close": { + "$ref": "#/$defs/positiveDecimal" + }, + "volume": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/unsignedDecimal" + } + ] + } + } + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": [ + "currency", + "rate" + ], + "properties": { + "currency": { + "$ref": "#/$defs/identifier" + }, + "rate": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "action_id", + "instrument_id", + "numerator", + "denominator" + ], + "properties": { + "type": { + "const": "split" + }, + "action_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "numerator": { + "$ref": "#/$defs/sequence" + }, + "denominator": { + "$ref": "#/$defs/sequence" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "action_id", + "instrument_id", + "amount_per_unit" + ], + "properties": { + "type": { + "const": "cash_dividend" + }, + "action_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "amount_per_unit": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/fractionalPolicy" } + } + } + ] + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": [ + "slice_sequence", + "start_at", + "end_at", + "available_at", + "received_at", + "bars", + "fx_rates", + "corporate_actions", + "borrow_observations", + "cash_rate_observations", + "settlement_failures", + "lifecycle_events", + "market_events", + "order_book_events" + ], + "properties": { + "slice_sequence": { + "$ref": "#/$defs/sequence" + }, + "start_at": { + "$ref": "#/$defs/timestamp" + }, + "end_at": { + "$ref": "#/$defs/timestamp" + }, + "available_at": { + "$ref": "#/$defs/timestamp" + }, + "received_at": { + "$ref": "#/$defs/timestamp" + }, + "bars": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/bar" + } + }, + "fx_rates": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/fxRate" + } + }, + "corporate_actions": { + "type": "array", + "items": { + "$ref": "#/$defs/corporateAction" + } + }, + "borrow_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/lifecycleEvent" + } + }, + "market_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketEvent" + } + }, + "order_book_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/orderBookEvent" + } + } + } + }, + "settlementInstruction": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "fill_id", "instrument_id", "currency", "cash_movement", "position_movement", "trade_date", "due_date", "status", "settled_at", "failed_at", "failure_reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "fill_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "cash_movement": { "$ref": "#/$defs/signedDecimal" }, + "position_movement": { "$ref": "#/$defs/signedDecimal" }, + "trade_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "due_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "status": { "enum": ["pending", "settled", "failed"] }, + "settled_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failed_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] }, + "failure_reason": { "oneOf": [{ "type": "null" }, { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" }] } + } + }, + "targetPortfolio": { + "type": "object", + "additionalProperties": false, + "required": [ + "basis", + "targets" + ], + "properties": { + "basis": { + "enum": [ + "weights", + "quantities" + ] + }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "weight", + "quantity", + "reference_price" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "weight": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "reference_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + } + } + } + } + } + }, + "order": { + "type": "object", + "additionalProperties": false, + "required": [ + "order_id", + "instrument_id", + "side", + "quantity", + "order_kind", + "trigger_price", + "limit_price", + "time_in_force", + "venue_id", + "calendar_id", + "expires_at", + "origin", + "created_event_id", + "updated_event_id", + "created_sequence", + "created_at", + "eligible_after_slice_sequence", + "triggered_at", + "triggered_slice_sequence", + "filled_quantity", + "filled_notional", + "status", + "rejection_reason" + ], + "properties": { + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "order_kind": { + "enum": [ + "market", + "limit", + "stop", + "stop_limit" + ] + }, + "trigger_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "limit_price": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/positiveDecimal" + } + ] + }, + "time_in_force": { + "enum": [ + "gtc", + "ioc", + "fok", + "day", + "gtd" + ] + }, + "venue_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "calendar_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/identifier" + } + ] + }, + "expires_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "origin": { + "enum": [ + "direct", + "target_rebalance", + "margin_liquidation", + "borrow_recall", + "instrument_halt", + "instrument_terminal" + ] + }, + "created_event_id": { + "$ref": "#/$defs/identifier" + }, + "updated_event_id": { + "$ref": "#/$defs/identifier" + }, + "created_sequence": { + "$ref": "#/$defs/sequence" + }, + "created_at": { + "$ref": "#/$defs/timestamp" + }, + "eligible_after_slice_sequence": { + "$ref": "#/$defs/nonnegativeSequence" + }, + "triggered_at": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/timestamp" + } + ] + }, + "triggered_slice_sequence": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sequence" + } + ] + }, + "filled_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "filled_notional": { + "$ref": "#/$defs/unsignedDecimal" + }, + "status": { + "enum": [ + "working", + "partially_filled", + "filled", + "cancelled", + "rejected" + ] + }, + "rejection_reason": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1 + } + ] + } + } + }, + "orderCancelled": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "reason" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "reason": { + "enum": [ + "strategy_requested", + "target_replaced", + "market_ioc", + "immediate_or_cancel", + "fill_or_kill", + "day_expired", + "gtd_expired", + "margin_call", + "borrow_recall" + ] + } + } + }, + "splitApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "previous_quantity", + "adjusted_quantity" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "previous_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "adjusted_quantity": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "dividendApplied": { + "type": "object", + "additionalProperties": false, + "required": [ + "action", + "quantity", + "cash_amount" + ], + "properties": { + "action": { + "$ref": "#/$defs/corporateAction" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "distributionApplied": { + "type": "object", + "additionalProperties": false, + "required": ["action", "source_quantity", "destination_quantity", "fractional_quantity", "allocated_basis", "fractional_basis", "cash_in_lieu"], + "properties": { + "action": { "$ref": "#/$defs/corporateAction" }, + "source_quantity": { "$ref": "#/$defs/signedDecimal" }, + "destination_quantity": { "$ref": "#/$defs/signedDecimal" }, + "fractional_quantity": { "$ref": "#/$defs/signedDecimal" }, + "allocated_basis": { "$ref": "#/$defs/signedDecimal" }, + "fractional_basis": { "$ref": "#/$defs/signedDecimal" }, + "cash_in_lieu": { "$ref": "#/$defs/signedDecimal" } + } + }, + "lifecycleApplied": { + "type": "object", + "additionalProperties": false, + "required": ["lifecycle_event", "listing", "liquidated_quantity", "cash_amount"], + "properties": { + "lifecycle_event": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/lifecycleEvent" }, + "listing": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "status", "provider_mappings"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "status": { "enum": ["tradable", "halted", "expired", "delisted"] }, + "provider_mappings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["provider", "provider_instrument_id"], + "properties": { + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + } + } + } + }, + "liquidated_quantity": { "$ref": "#/$defs/signedDecimal" }, + "cash_amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "orderAdjusted": { + "type": "object", + "additionalProperties": false, + "required": [ + "order", + "action_id" + ], + "properties": { + "order": { + "$ref": "#/$defs/order" + }, + "action_id": { + "$ref": "#/$defs/identifier" + } + } + }, + "executionPriceSelected": { + "type": "object", + "additionalProperties": false, + "required": ["order_id", "instrument_id", "side", "reference_price", "spread_adjustment", "impact_adjustment", "final_price"], + "properties": { + "order_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "reference_price": { "$ref": "#/$defs/positiveDecimal" }, + "spread_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, + "impact_adjustment": { "$ref": "#/$defs/unsignedDecimal" }, + "final_price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "fill": { + "type": "object", + "additionalProperties": false, + "required": [ + "fill_id", + "order_id", + "instrument_id", + "quote_currency", + "side", + "quantity", + "price", + "notional", + "fee", + "executed_at", + "slice_sequence", + "fee_components" + ], + "properties": { + "fill_id": { + "$ref": "#/$defs/identifier" + }, + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "side": { + "enum": [ + "buy", + "sell" + ] + }, + "quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "price": { + "$ref": "#/$defs/positiveDecimal" + }, + "notional": { + "$ref": "#/$defs/positiveDecimal" + }, + "fee": { + "$ref": "#/$defs/signedDecimal" + }, + "fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/calculatedFeeComponent" + } + }, + "executed_at": { + "$ref": "#/$defs/timestamp" + }, + "slice_sequence": { + "$ref": "#/$defs/sequence" + } + } + }, + "calculatedFeeComponent": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "currency", + "amount", + "quote_amount" + ], + "properties": { + "name": { + "$ref": "#/$defs/identifier" + }, + "kind": { + "enum": [ + "fixed", + "notional_bps", + "per_unit", + "minimum_adjustment", + "maximum_adjustment" + ] + }, + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "quote_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "feeComponentAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "kind", + "currency", + "amount", + "quote_currency", + "quote_amount", + "base_amount" + ], + "properties": { + "name": { + "$ref": "#/$defs/identifier" + }, + "kind": { + "enum": [ + "fixed", + "notional_bps", + "per_unit", + "minimum_adjustment", + "maximum_adjustment" + ] + }, + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "quote_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "base_amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "quantityThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "moneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "ratioThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "ratio" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "basisPointsThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "unit", + "value" + ], + "properties": { + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + } + }, + "instrumentQuantityThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "quantity" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "instrumentMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "currencyMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "unit", "value"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "money" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "settlementPositionThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "unit", "value"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "unit": { "const": "quantity" }, + "value": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "instrumentBasisPointsThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "unit", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "basis_points" + }, + "value": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + } + } + }, + "instrumentShortingThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "value" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "value": { + "const": false + } + } + }, + "groupMoneyThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "unit", + "value" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "money" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "groupRatioThreshold": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "unit", + "value" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "unit": { + "const": "ratio" + }, + "value": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "fillClipReason": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_order_quantity" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_long_position" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_short_position" + }, + "threshold": { + "$ref": "#/$defs/quantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_gross_exposure" + }, + "threshold": { + "$ref": "#/$defs/moneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "max_leverage" + }, + "threshold": { + "$ref": "#/$defs/ratioThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "initial_margin" + }, + "threshold": { + "$ref": "#/$defs/basisPointsThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_long_position" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["version", "policy", "threshold"], + "properties": { + "version": { "const": "1" }, + "policy": { "const": "settlement_cash_buying_power" }, + "threshold": { "$ref": "#/$defs/currencyMoneyThreshold" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["version", "policy", "threshold"], + "properties": { + "version": { "const": "1" }, + "policy": { "const": "settlement_position_availability" }, + "threshold": { "$ref": "#/$defs/settlementPositionThreshold" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_short_position" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_max_notional_exposure" + }, + "threshold": { + "$ref": "#/$defs/instrumentMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_shorting_disabled" + }, + "threshold": { + "$ref": "#/$defs/instrumentShortingThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_borrow_availability" + }, + "threshold": { + "$ref": "#/$defs/instrumentQuantityThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "instrument_initial_margin" + }, + "threshold": { + "$ref": "#/$defs/instrumentBasisPointsThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_gross_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_long_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_short_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_absolute_net_exposure" + }, + "threshold": { + "$ref": "#/$defs/groupMoneyThreshold" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "policy", + "threshold" + ], + "properties": { + "version": { + "const": "1" + }, + "policy": { + "const": "group_max_concentration" + }, + "threshold": { + "$ref": "#/$defs/groupRatioThreshold" + } + } + } + ] + }, + "fillClipped": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason", + "order_id", + "instrument_id", + "proposed_quantity", + "permitted_quantity", + "price" + ], + "properties": { + "reason": { + "$ref": "#/$defs/fillClipReason" + }, + "order_id": { + "$ref": "#/$defs/identifier" + }, + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "proposed_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "permitted_quantity": { + "$ref": "#/$defs/unsignedDecimal" + }, + "price": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "borrowFee": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "short_quantity", + "reference_price", + "borrow_bps", + "period_start", + "period_end", + "fee" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "reference_price": { + "$ref": "#/$defs/positiveDecimal" + }, + "borrow_bps": { + "type": "integer", + "minimum": 1, + "maximum": 10000 + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "fee": { + "$ref": "#/$defs/positiveDecimal" + } + } + }, + "borrowCharge": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "quote_currency", + "short_quantity", + "reference_price", + "day_count", + "compounding", + "period_start", + "period_end", + "amount" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "reference_price": { + "$ref": "#/$defs/positiveDecimal" + }, + "day_count": { + "enum": [ + "actual_365", + "actual_360" + ] + }, + "compounding": { + "enum": [ + "simple", + "daily" + ] + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "borrowRecall": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "short_quantity", + "close_out_quantity" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/borrowObservation" + }, + "short_quantity": { + "$ref": "#/$defs/positiveDecimal" + }, + "close_out_quantity": { + "$ref": "#/$defs/unsignedDecimal" + } + } + }, + "cashInterest": { + "type": "object", + "additionalProperties": false, + "required": [ + "observation", + "opening_balance", + "applied_rate_bps", + "day_count", + "compounding", + "period_start", + "period_end", + "amount", + "closing_balance" + ], + "properties": { + "observation": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/cashRateObservation" + }, + "opening_balance": { + "$ref": "#/$defs/signedDecimal" + }, + "applied_rate_bps": { + "type": "integer", + "minimum": -1000000, + "maximum": 1000000 + }, + "day_count": { + "enum": [ + "actual_365", + "actual_360" + ] + }, + "compounding": { + "enum": [ + "simple", + "daily" + ] + }, + "period_start": { + "$ref": "#/$defs/timestamp" + }, + "period_end": { + "$ref": "#/$defs/timestamp" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "closing_balance": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "cashAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "currency", + "amount", + "fx_rate", + "base_value", + "interest", + "base_interest", + "settled_amount", + "unsettled_amount", + "base_settled_value", + "base_unsettled_value" + ], + "properties": { + "currency": { + "$ref": "#/$defs/identifier" + }, + "amount": { + "$ref": "#/$defs/signedDecimal" + }, + "fx_rate": { + "$ref": "#/$defs/positiveDecimal" + }, + "base_value": { + "$ref": "#/$defs/signedDecimal" + }, + "interest": { + "$ref": "#/$defs/signedDecimal" + }, + "base_interest": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_amount": { + "$ref": "#/$defs/signedDecimal" + }, + "base_settled_value": { + "$ref": "#/$defs/signedDecimal" + }, + "base_unsettled_value": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "positionAttribution": { + "type": "object", + "additionalProperties": false, + "required": [ + "instrument_id", + "quote_currency", + "quantity", + "settled_quantity", + "unsettled_quantity", + "mark", + "fx_rate", + "market_value", + "base_market_value", + "cost_basis", + "base_cost_basis", + "realized_pnl", + "base_realized_pnl", + "unrealized_pnl", + "base_unrealized_pnl", + "dividend_pnl", + "base_dividend_pnl", + "execution_fees", + "base_execution_fees", + "borrow_fees", + "base_borrow_fees", + "total_fees", + "base_total_fees", + "execution_fee_components" + ], + "properties": { + "instrument_id": { + "$ref": "#/$defs/identifier" + }, + "quote_currency": { + "$ref": "#/$defs/identifier" + }, + "quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_quantity": { + "$ref": "#/$defs/signedDecimal" + }, + "mark": { + "$ref": "#/$defs/positiveDecimal" + }, + "fx_rate": { + "$ref": "#/$defs/positiveDecimal" + }, + "market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "base_market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "base_cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "base_dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "base_total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/feeComponentAttribution" + } + } + } + }, + "margin": { + "type": "object", + "additionalProperties": false, + "required": [ + "initial_requirement", + "maintenance_requirement", + "initial_excess", + "maintenance_excess", + "margin_call" + ], + "properties": { + "initial_requirement": { + "$ref": "#/$defs/unsignedDecimal" + }, + "maintenance_requirement": { + "$ref": "#/$defs/unsignedDecimal" + }, + "initial_excess": { + "$ref": "#/$defs/signedDecimal" + }, + "maintenance_excess": { + "$ref": "#/$defs/signedDecimal" + }, + "margin_call": { + "type": "boolean" + } + } + }, + "groupExposure": { + "type": "object", + "additionalProperties": false, + "required": [ + "group_id", + "gross_exposure", + "net_exposure", + "long_exposure", + "short_exposure", + "concentration" + ], + "properties": { + "group_id": { + "$ref": "#/$defs/identifier" + }, + "gross_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "net_exposure": { + "$ref": "#/$defs/signedDecimal" + }, + "long_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "short_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "concentration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/signedDecimal" + } + ] + } + } + }, + "valuation": { + "type": "object", + "additionalProperties": false, + "required": [ + "base_currency", + "cash", + "settled_cash", + "unsettled_cash", + "net_market_value", + "long_market_value", + "short_market_value", + "gross_exposure", + "cost_basis", + "realized_pnl", + "unrealized_pnl", + "equity", + "dividend_pnl", + "execution_fees", + "borrow_fees", + "cash_interest", + "total_fees", + "cash_balances", + "positions", + "margin", + "group_exposures", + "execution_fee_components" + ], + "properties": { + "base_currency": { + "$ref": "#/$defs/identifier" + }, + "cash": { + "$ref": "#/$defs/signedDecimal" + }, + "settled_cash": { + "$ref": "#/$defs/signedDecimal" + }, + "unsettled_cash": { + "$ref": "#/$defs/signedDecimal" + }, + "net_market_value": { + "$ref": "#/$defs/signedDecimal" + }, + "long_market_value": { + "$ref": "#/$defs/unsignedDecimal" + }, + "short_market_value": { + "$ref": "#/$defs/unsignedDecimal" + }, + "gross_exposure": { + "$ref": "#/$defs/unsignedDecimal" + }, + "cost_basis": { + "$ref": "#/$defs/signedDecimal" + }, + "realized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "unrealized_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "equity": { + "$ref": "#/$defs/signedDecimal" + }, + "dividend_pnl": { + "$ref": "#/$defs/signedDecimal" + }, + "execution_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "borrow_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "total_fees": { + "$ref": "#/$defs/signedDecimal" + }, + "cash_balances": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/cashAttribution" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/$defs/positionAttribution" + } + }, + "margin": { + "$ref": "#/$defs/margin" + }, + "group_exposures": { + "type": "array", + "items": { + "$ref": "#/$defs/groupExposure" + } + }, + "execution_fee_components": { + "type": "array", + "items": { + "$ref": "#/$defs/feeComponentAttribution" + } + }, + "cash_interest": { + "$ref": "#/$defs/signedDecimal" + } + } + }, + "intentRejected": { + "type": "object", + "additionalProperties": false, + "required": [ + "reason" + ], + "properties": { + "reason": { + "type": "string", + "minLength": 1 + } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "runCompleted": { + "type": "object", + "additionalProperties": false, + "required": [ + "scenario_sha256", + "execution_model", + "valuation", + "order_counts" + ], + "properties": { + "scenario_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "execution_model": { + "enum": ["completed_bar_v1", "completed_bar_next_open_v1", "completed_bar_adverse_touch_v1", "quote_trade_v1", "order_book_v1"] + }, + "valuation": { + "$ref": "#/$defs/valuation" + }, + "order_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "total", + "active", + "filled", + "rejected", + "cancelled" + ], + "properties": { + "total": { + "type": "integer", + "minimum": 0 + }, + "active": { + "type": "integer", + "minimum": 0 + }, + "filled": { + "type": "integer", + "minimum": 0 + }, + "rejected": { + "type": "integer", + "minimum": 0 + }, + "cancelled": { + "type": "integer", + "minimum": 0 + } + } + } + } + } + } +} diff --git a/contracts/v15/scenario-stream.schema.json b/contracts/v15/scenario-stream.schema.json new file mode 100644 index 0000000..5af7c61 --- /dev/null +++ b/contracts/v15/scenario-stream.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v15/scenario-stream.schema.json", + "title": "Trading Engine v15 replay scenario stream record", + "description": "Market-slice records are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-record rule is enforced semantically.", + "oneOf": [ + { "$ref": "#/$defs/headerRecord" }, + { "$ref": "#/$defs/sliceRecord" }, + { "$ref": "#/$defs/endRecord" } + ], + "$defs": { + "headerRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "15" }, + "scenario_sequence": { "const": "1" }, + "record_type": { "const": "scenario_header" }, + "payload": { "$ref": "#/$defs/headerPayload" } + } + }, + "sliceRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "15" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/sequence" }, + "record_type": { "const": "market_slice" }, + "payload": { "$ref": "#/$defs/slicePayload" } + } + }, + "endRecord": { + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "scenario_sequence", "record_type", "payload"], + "properties": { + "contract_version": { "const": "15" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/sequence" }, + "record_type": { "const": "scenario_end" }, + "payload": { + "type": "object", + "additionalProperties": false, + "required": ["slice_count"], + "properties": { "slice_count": { "type": "string", "pattern": "^(?:0|[1-9][0-9]*)$" } } + } + } + }, + "headerPayload": { + "type": "object", + "additionalProperties": false, + "required": ["metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events"], + "properties": { + "metadata": { "type": "object" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 } + } + }, + "slicePayload": { + "type": "object", + "additionalProperties": false, + "required": ["market_slice", "intents"], + "properties": { + "market_slice": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v15/scenario.schema.json b/contracts/v15/scenario.schema.json new file mode 100644 index 0000000..ffab82b --- /dev/null +++ b/contracts/v15/scenario.schema.json @@ -0,0 +1,870 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v15/scenario.schema.json", + "title": "Trading Engine v15 replay scenario", + "description": "Strict deterministic scenario contract with explicit venue-local session policies resolved to absolute instants outside the reducer.", + "type": "object", + "additionalProperties": false, + "required": ["contract_version", "metadata", "run_id", "base_currency", "initial_portfolio", "instruments", "venue_calendars", "risk", "execution", "financing", "settlement", "max_internal_events", "schedule", "slices"], + "properties": { + "contract_version": { "const": "15" }, + "metadata": { "type": "object" }, + "run_id": { "$ref": "#/$defs/identifier" }, + "base_currency": { "$ref": "#/$defs/identifier" }, + "initial_portfolio": { "$ref": "#/$defs/initialPortfolio" }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueCalendar" } + }, + "risk": { "$ref": "#/$defs/risk" }, + "execution": { "$ref": "#/$defs/execution" }, + "financing": { "$ref": "#/$defs/financing" }, + "settlement": { "$ref": "#/$defs/settlement" }, + "max_internal_events": { "type": "integer", "minimum": 1, "maximum": 100000 }, + "schedule": { "type": "array", "items": { "$ref": "#/$defs/scheduleItem" } }, + "slices": { + "description": "Slices are ordered and non-overlapping: each start_at is at or after the prior end_at. Equal boundaries are valid. This cross-item rule is enforced semantically.", + "type": "array", + "items": { "$ref": "#/$defs/marketSlice" } + } + }, + "$defs": { + "settlement": { + "type": "object", + "additionalProperties": false, + "required": ["cash_buying_power", "position_availability", "calendars", "rules"], + "properties": { + "cash_buying_power": { "enum": ["total_cash", "settled_cash"] }, + "position_availability": { "enum": ["total_positions", "settled_positions"] }, + "calendars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementCalendar" } }, + "rules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/settlementRule" } } + } + }, + "settlementCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "version", "business_dates"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "version": { "const": "1" }, + "business_dates": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" } } + } + }, + "settlementRule": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "calendar_id", "lag_business_days"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "calendar_id": { "$ref": "#/$defs/identifier" }, + "lag_business_days": { "type": "integer", "minimum": 0, "maximum": 30 } + } + }, + "settlementFailure": { + "type": "object", + "additionalProperties": false, + "required": ["instruction_id", "reason"], + "properties": { + "instruction_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1, "pattern": "^\\S(?:.*\\S)?$" } + } + }, + "financing": { + "type": "object", + "additionalProperties": false, + "required": ["day_count", "compounding", "borrow_missing_data", "cash_missing_data", "locate_policy", "recall_policy"], + "properties": { + "day_count": { "enum": ["actual_365", "actual_360"] }, + "compounding": { "enum": ["simple", "daily"] }, + "borrow_missing_data": { "enum": ["reject", "zero"] }, + "cash_missing_data": { "enum": ["reject", "zero"] }, + "locate_policy": { "enum": ["reject_order", "clip_fill"] }, + "recall_policy": { "enum": ["reject_new_shorts", "close_out"] } + } + }, + "borrowObservation": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "effective_at", "available_quantity", "annual_rate_bps", "recalled"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "available_quantity": { "$ref": "#/$defs/unsignedDecimal" }, + "annual_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "recalled": { "type": "boolean" } + } + }, + "cashRateObservation": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "effective_at", "credit_rate_bps", "debit_rate_bps"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "effective_at": { "$ref": "#/$defs/timestamp" }, + "credit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 }, + "debit_rate_bps": { "type": "integer", "minimum": -1000000, "maximum": 1000000 } + } + }, + "identifier": { + "type": "string", + "minLength": 1, + "pattern": "^[^\\s\\u0000-\\u001f\\u007f]+$" + }, + "signedDecimal": { + "type": "string", + "pattern": "^(?:0|-?(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?))$" + }, + "unsignedDecimal": { + "type": "string", + "pattern": "^(?:0|0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "positiveDecimal": { + "type": "string", + "pattern": "^(?:0[.][0-9]{0,5}[1-9]|[1-9][0-9]*(?:[.][0-9]{0,5}[1-9])?)$" + }, + "sequence": { "type": "string", "pattern": "^[1-9][0-9]*$" }, + "timestamp": { + "type": "string", + "format": "date-time", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:[.][0-9]{1,6})?(?:[zZ]|[+-][0-9]{2}:[0-9]{2})$" + }, + "cashBalance": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "amount"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "amount": { "$ref": "#/$defs/signedDecimal" } + } + }, + "initialPortfolio": { + "type": "object", + "additionalProperties": false, + "required": ["cash", "positions", "marks", "fx_rates"], + "properties": { + "cash": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/cashBalance" } }, + "positions": { "type": "array", "items": { "$ref": "#/$defs/initialPosition" } }, + "marks": { "type": "array", "items": { "$ref": "#/$defs/initialMark" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } } + } + }, + "initialPosition": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity", "cost_basis", "realized_pnl", "dividend_pnl", "execution_fees", "borrow_fees"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" }, + "cost_basis": { "$ref": "#/$defs/signedDecimal" }, + "realized_pnl": { "$ref": "#/$defs/signedDecimal" }, + "dividend_pnl": { "$ref": "#/$defs/signedDecimal" }, + "execution_fees": { "$ref": "#/$defs/unsignedDecimal" }, + "borrow_fees": { "$ref": "#/$defs/unsignedDecimal" } + } + }, + "initialMark": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "price"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "instrument": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "symbol", "quote_currency", "tick_size", "lot_size"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "quote_currency": { "$ref": "#/$defs/identifier" }, + "tick_size": { "$ref": "#/$defs/positiveDecimal" }, + "lot_size": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "venueCalendar": { + "type": "object", + "additionalProperties": false, + "required": ["calendar_id", "calendar_version", "venue_id", "instrument_ids", "sessions"], + "properties": { + "calendar_id": { "$ref": "#/$defs/identifier" }, + "calendar_version": { "const": "1" }, + "venue_id": { "$ref": "#/$defs/identifier" }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "sessions": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/venueSession" } + } + } + }, + "venueSession": { + "oneOf": [ + { "$ref": "#/$defs/openVenueSession" }, + { "$ref": "#/$defs/holidayVenueSession" } + ] + }, + "openVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "enum": ["regular", "early_close"] }, + "phases": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { "$ref": "#/$defs/venuePhase" } + } + } + }, + "holidayVenueSession": { + "type": "object", + "additionalProperties": false, + "required": ["session_date", "policy", "phases"], + "properties": { + "session_date": { "type": "string", "format": "date", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "policy": { "const": "holiday" }, + "phases": { "type": "array", "maxItems": 0 } + } + }, + "venuePhase": { + "type": "object", + "additionalProperties": false, + "required": ["phase", "opens_at", "closes_at"], + "properties": { + "phase": { "enum": ["premarket", "opening_auction", "regular", "closing_auction", "postmarket"] }, + "opens_at": { "$ref": "#/$defs/timestamp" }, + "closes_at": { "$ref": "#/$defs/timestamp" } + } + }, + "risk": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_leverage", "short_borrow_bps", "instrument_policies", "groups"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "max_leverage": { "$ref": "#/$defs/positiveDecimal" }, + "short_borrow_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "instrument_policies": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/instrumentRiskPolicy" } + }, + "groups": { + "type": "array", + "items": { "$ref": "#/$defs/riskGroup" } + } + } + }, + "instrumentRiskPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "max_order_quantity", "max_long_position", "max_short_position", "max_notional_exposure", "initial_margin_bps", "maintenance_margin_bps", "shorting_allowed"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "max_order_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "max_long_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_short_position": { "$ref": "#/$defs/positiveDecimal" }, + "max_notional_exposure": { "$ref": "#/$defs/positiveDecimal" }, + "initial_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "maintenance_margin_bps": { "type": "integer", "minimum": 1, "maximum": 10000 }, + "shorting_allowed": { "type": "boolean" } + } + }, + "nullablePositiveDecimal": { + "oneOf": [ + { "type": "null" }, + { "$ref": "#/$defs/positiveDecimal" } + ] + }, + "riskGroup": { + "type": "object", + "additionalProperties": false, + "required": ["group_id", "group_version", "group_type", "instrument_ids", "limits"], + "properties": { + "group_id": { "$ref": "#/$defs/identifier" }, + "group_version": { "const": "1" }, + "group_type": { "enum": ["issuer", "sector", "currency", "country", "asset_class", "custom"] }, + "instrument_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/identifier" } + }, + "limits": { "$ref": "#/$defs/riskGroupLimits" } + } + }, + "riskGroupLimits": { + "type": "object", + "additionalProperties": false, + "required": ["max_gross_exposure", "max_long_exposure", "max_short_exposure", "max_absolute_net_exposure", "max_concentration"], + "properties": { + "max_gross_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_long_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_short_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_absolute_net_exposure": { "$ref": "#/$defs/nullablePositiveDecimal" }, + "max_concentration": { + "oneOf": [ + { "type": "null" }, + { "allOf": [{ "$ref": "#/$defs/positiveDecimal" }, { "pattern": "^(?:0[.][0-9]{0,5}[1-9]|1)$" }] } + ] + } + } + }, + "execution": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "completed_bar_v1" }, + "configuration": { "$ref": "#/$defs/completedBarV1Configuration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "enum": ["completed_bar_next_open_v1", "completed_bar_adverse_touch_v1"] }, + "configuration": { "$ref": "#/$defs/conservativeBarConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "quote_trade_v1" }, + "configuration": { "$ref": "#/$defs/quoteTradeConfiguration" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["model", "configuration"], + "properties": { + "model": { "const": "order_book_v1" }, + "configuration": { "$ref": "#/$defs/orderBookConfiguration" } + } + } + ] + }, + "completedBarV1Configuration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "2" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "conservativeBarConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "spread_model", "impact_model"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "spread_model": { "$ref": "#/$defs/fixedSpreadModel" }, + "impact_model": { "$ref": "#/$defs/linearImpactModel" } + } + }, + "quoteTradeConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } } + } + }, + "orderBookConfiguration": { + "type": "object", + "additionalProperties": false, + "required": ["version", "participation_bps", "fee_schedules", "max_depth_levels"], + "properties": { + "version": { "const": "1" }, + "participation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fee_schedules": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeSchedule" } }, + "max_depth_levels": { "type": "integer", "minimum": 1, "maximum": 1024 } + } + }, + "fixedSpreadModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "half_spread_bps"], + "properties": { + "model": { "const": "fixed_half_spread_v1" }, + "half_spread_bps": { "type": "integer", "minimum": 0, "maximum": 10000 } + } + }, + "linearImpactModel": { + "type": "object", + "additionalProperties": false, + "required": ["model", "coefficient_bps", "missing_volume_policy"], + "properties": { + "model": { "const": "linear_participation_v1" }, + "coefficient_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "missing_volume_policy": { "enum": ["reject", "zero_impact"] } + } + }, + "feeSchedule": { + "type": "object", + "additionalProperties": false, + "required": ["schedule_id", "instrument_id", "settlement_currency", "minimum", "maximum", "components"], + "properties": { + "schedule_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "settlement_currency": { "$ref": "#/$defs/identifier" }, + "minimum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "maximum": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] }, + "components": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/feeComponent" } } + } + }, + "feeComponent": { + "type": "object", + "additionalProperties": false, + "required": ["name", "currency", "kind", "value", "rounding", "applies_to"], + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "currency": { "$ref": "#/$defs/identifier" }, + "kind": { "enum": ["fixed", "notional_bps", "per_unit"] }, + "value": { "oneOf": [{ "$ref": "#/$defs/signedDecimal" }, { "type": "integer", "minimum": -10000, "maximum": 10000 }] }, + "rounding": { "enum": ["up", "down", "nearest"] }, + "applies_to": { "enum": ["any", "maker", "taker"] } + }, + "allOf": [ + { "if": { "properties": { "kind": { "const": "notional_bps" } } }, "then": { "properties": { "value": { "type": "integer" } } } }, + { "if": { "properties": { "kind": { "enum": ["fixed", "per_unit"] } } }, "then": { "properties": { "value": { "$ref": "#/$defs/signedDecimal" } } } } + ] + }, + "scheduleItem": { + "type": "object", + "additionalProperties": false, + "required": ["after_slice_sequence", "intents"], + "properties": { + "after_slice_sequence": { "$ref": "#/$defs/sequence" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "#/$defs/intent" } } + } + }, + "intent": { + "oneOf": [ + { "$ref": "#/$defs/targetWeights" }, + { "$ref": "#/$defs/targetQuantities" }, + { "$ref": "#/$defs/submitOrder" }, + { "$ref": "#/$defs/cancelOrder" }, + { "$ref": "#/$defs/metric" } + ] + }, + "targetWeights": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_weights" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "weight"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "weight": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "targetQuantities": { + "type": "object", + "additionalProperties": false, + "required": ["type", "targets"], + "properties": { + "type": { "const": "target_quantities" }, + "targets": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "quantity"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "quantity": { "$ref": "#/$defs/signedDecimal" } + } + } + } + } + }, + "submitOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "side", "quantity", "order_kind", "trigger_price", "limit_price", "time_in_force", "venue_id", "calendar_id", "expires_at"], + "properties": { + "type": { "const": "submit_order" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "side": { "enum": ["buy", "sell"] }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "order_kind": { "enum": ["market", "limit", "stop", "stop_limit"] }, + "trigger_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "limit_price": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/positiveDecimal" }] }, + "time_in_force": { "enum": ["gtc", "ioc", "fok", "day", "gtd"] }, + "venue_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "calendar_id": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/identifier" }] }, + "expires_at": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/timestamp" }] } + }, + "allOf": [ + { "if": { "properties": { "order_kind": { "const": "market" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "limit" } } }, "then": { "properties": { "trigger_price": { "type": "null" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "type": "null" } } } }, + { "if": { "properties": { "order_kind": { "const": "stop_limit" } } }, "then": { "properties": { "trigger_price": { "$ref": "#/$defs/positiveDecimal" }, "limit_price": { "$ref": "#/$defs/positiveDecimal" } } } }, + { "if": { "properties": { "time_in_force": { "const": "day" } } }, "then": { "properties": { "venue_id": { "$ref": "#/$defs/identifier" }, "calendar_id": { "$ref": "#/$defs/identifier" }, "expires_at": { "type": "null" } } } }, + { "if": { "properties": { "time_in_force": { "const": "gtd" } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "$ref": "#/$defs/timestamp" } } } }, + { "if": { "properties": { "time_in_force": { "enum": ["gtc", "ioc", "fok"] } } }, "then": { "properties": { "venue_id": { "type": "null" }, "calendar_id": { "type": "null" }, "expires_at": { "type": "null" } } } } + ] + }, + "cancelOrder": { + "type": "object", + "additionalProperties": false, + "required": ["type", "order_id"], + "properties": { + "type": { "const": "cancel_order" }, + "order_id": { "$ref": "#/$defs/identifier" } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": ["type", "name", "value"], + "properties": { + "type": { "const": "emit_metric" }, + "name": { "type": "string" }, + "value": { "type": "string" } + } + }, + "marketSlice": { + "type": "object", + "additionalProperties": false, + "required": ["slice_sequence", "start_at", "end_at", "available_at", "received_at", "bars", "market_events", "order_book_events", "fx_rates", "corporate_actions", "borrow_observations", "cash_rate_observations", "settlement_failures", "lifecycle_events"], + "properties": { + "slice_sequence": { "$ref": "#/$defs/sequence" }, + "start_at": { "$ref": "#/$defs/timestamp" }, + "end_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "bars": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/bar" } }, + "market_events": { "type": "array", "items": { "$ref": "#/$defs/marketEvent" } }, + "order_book_events": { "type": "array", "items": { "$ref": "#/$defs/orderBookEvent" } }, + "fx_rates": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/fxRate" } }, + "corporate_actions": { "type": "array", "items": { "$ref": "#/$defs/corporateAction" } }, + "borrow_observations": { "type": "array", "items": { "$ref": "#/$defs/borrowObservation" } }, + "cash_rate_observations": { "type": "array", "items": { "$ref": "#/$defs/cashRateObservation" } }, + "settlement_failures": { "type": "array", "items": { "$ref": "#/$defs/settlementFailure" } }, + "lifecycle_events": { "type": "array", "items": { "$ref": "#/$defs/lifecycleEvent" } } + } + }, + "bar": { + "type": "object", + "additionalProperties": false, + "required": ["instrument_id", "open", "high", "low", "close", "volume"], + "properties": { + "instrument_id": { "$ref": "#/$defs/identifier" }, + "open": { "$ref": "#/$defs/positiveDecimal" }, + "high": { "$ref": "#/$defs/positiveDecimal" }, + "low": { "$ref": "#/$defs/positiveDecimal" }, + "close": { "$ref": "#/$defs/positiveDecimal" }, + "volume": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/unsignedDecimal" }] } + } + }, + "marketEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "bid_price", "bid_quantity", "ask_price", "ask_quantity"], + "properties": { + "type": { "const": "quote" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "bid_price": { "$ref": "#/$defs/positiveDecimal" }, + "bid_quantity": { "$ref": "#/$defs/positiveDecimal" }, + "ask_price": { "$ref": "#/$defs/positiveDecimal" }, + "ask_quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "orderBookLevel": { + "type": "object", + "additionalProperties": false, + "required": ["price", "quantity"], + "properties": { + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "orderBookEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "bids", "asks"], + "properties": { + "type": { "const": "snapshot" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "bids": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } }, + "asks": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/orderBookLevel" } } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price", "quantity"], + "properties": { + "type": { "const": "set" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "side", "price"], + "properties": { + "type": { "const": "delete" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "side": { "enum": ["bid", "ask"] }, + "price": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "instrument_id", "event_at", "available_at", "received_at", "ingest_sequence", "book_sequence", "price", "quantity", "aggressor_side"], + "properties": { + "type": { "const": "trade" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "event_at": { "$ref": "#/$defs/timestamp" }, + "available_at": { "$ref": "#/$defs/timestamp" }, + "received_at": { "$ref": "#/$defs/timestamp" }, + "ingest_sequence": { "$ref": "#/$defs/sequence" }, + "book_sequence": { "$ref": "#/$defs/sequence" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "quantity": { "$ref": "#/$defs/positiveDecimal" }, + "aggressor_side": { "enum": ["buy", "sell", "unknown"] } + } + } + ] + }, + "fxRate": { + "type": "object", + "additionalProperties": false, + "required": ["currency", "rate"], + "properties": { + "currency": { "$ref": "#/$defs/identifier" }, + "rate": { "$ref": "#/$defs/positiveDecimal" } + } + }, + "corporateAction": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "numerator", "denominator"], + "properties": { + "type": { "const": "split" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "amount_per_unit"], + "properties": { + "type": { "const": "cash_dividend" }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "amount_per_unit": { "$ref": "#/$defs/positiveDecimal" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "action_id", "instrument_id", "destination_instrument_id", "numerator", "denominator", "basis_allocation_bps", "fractional_policy"], + "properties": { + "type": { "enum": ["stock_dividend", "rights", "spin_off"] }, + "action_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "destination_instrument_id": { "$ref": "#/$defs/identifier" }, + "numerator": { "$ref": "#/$defs/sequence" }, + "denominator": { "$ref": "#/$defs/sequence" }, + "basis_allocation_bps": { "type": "integer", "minimum": 0, "maximum": 10000 }, + "fractional_policy": { "$ref": "#/$defs/fractionalPolicy" } + } + } + ] + }, + "fractionalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "reject" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_in_lieu" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "terminalPolicy": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["policy"], + "properties": { "policy": { "const": "hold" } } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["policy", "price", "currency"], + "properties": { + "policy": { "const": "cash_out" }, + "price": { "$ref": "#/$defs/positiveDecimal" }, + "currency": { "$ref": "#/$defs/identifier" } + } + } + ] + }, + "lifecycleEvent": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "reason"], + "properties": { + "type": { "const": "halt" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "reason": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id"], + "properties": { + "type": { "const": "resume" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "symbol", "provider", "provider_instrument_id"], + "properties": { + "type": { "const": "identifier_change" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "symbol": { "$ref": "#/$defs/identifier" }, + "provider": { "$ref": "#/$defs/identifier" }, + "provider_instrument_id": { "$ref": "#/$defs/identifier" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy"], + "properties": { + "type": { "const": "expiration" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "event_id", "instrument_id", "terminal_policy", "reason"], + "properties": { + "type": { "const": "delisting" }, + "event_id": { "$ref": "#/$defs/identifier" }, + "instrument_id": { "$ref": "#/$defs/identifier" }, + "terminal_policy": { "$ref": "#/$defs/terminalPolicy" }, + "reason": { "type": "string", "minLength": 1 } + } + } + ] + } + } +} diff --git a/docs/api-reference.md b/docs/api-reference.md index 52529d2..a5d0aed 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -7,4 +7,4 @@ that every public interface has a corresponding page. The generated reference describes library types and functions. The versioned JSON and JSON Lines -files under [Contracts](../contracts/v14/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v15/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index ef3971a..aff839c 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,7 +21,7 @@ Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill canonical fixtures. Standard output and standard error are captured separately because human diagnostics may contain platform-specific paths or process details and are not part of the journal contract. -The full test suite additionally validates and replays the current v14 batch, stream, journal, and +The full test suite additionally validates and replays the current v15 batch, stream, journal, and strategy-v12 fixtures, including quote/trade causality and the reconciled first valuation. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs diff --git a/docs/execution-model.md b/docs/execution-model.md index 597573e..53d70d6 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,12 +1,12 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v14 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, -`completed_bar_adverse_touch_v1`, and `quote_trade_v1`; embedders can inject another module through +Contract v15 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, +`completed_bar_adverse_touch_v1`, `quote_trade_v1`, and `order_book_v1`; embedders can inject another module through the typed engine configuration without introducing runtime shared-library loading. The selected name is repeated in both terminal audit records. -Each compiled model owns a strict configuration contract. The v14 envelope separates selection from +Each compiled model owns a strict configuration contract. The v15 envelope separates selection from model-specific parameters: ```json @@ -59,6 +59,18 @@ sells consume only buy-aggressor trades at or above it. An `unknown` aggressor n passive fill. Each event has independent, lot-rounded capacity, and its `event_at` is the fill's economic timestamp. Completed bars remain required solely for synchronized valuation. +The order-book model uses configuration version `"1"`, adding `max_depth_levels` from 1 through +1,024. Each instrument's slice-local bundle begins with a full bid/ask snapshot and uses contiguous +absolute set, delete, and trade updates. Crossed states, gaps, missing deletes, and states beyond +the depth limit fail replay; a locked best bid and ask is accepted. Each later slice starts from a +fresh snapshot, so no unbounded or hidden book state survives a slice boundary. + +Marketable orders walk observable opposite-side levels in price order. Passive limits start behind +the displayed quantity at their price and behind earlier engine orders. Reductions decrease queue +ahead, additions join behind, and an aggressor-qualified trade consumes queue ahead before filling +the order. This model has its own liquidity state and does not reuse completed-bar or quote/trade +fill semantics. Bars remain mandatory only for valuation. + ## Eligibility An order records the slice after which it is eligible. The matcher requires: diff --git a/docs/persistra.md b/docs/persistra.md index 68864ad..f66b6be 100644 --- a/docs/persistra.md +++ b/docs/persistra.md @@ -54,12 +54,12 @@ lifecycle belong to Persistra. Persistra currently uses the transitional v3 [scenario](../contracts/v3/scenario.schema.json) and [journal](../contracts/v3/journal.schema.json) schemas and their adjacent conformance fixtures for -structural checks. The engine advertises current contract v14 while retaining v13 through v3 and +structural checks. The engine advertises current contract v15 while retaining v14 through v3 and exact v3 journal output for v3 inputs. The engine parser is authoritative for ordering, catalog coverage, causality, tick, lot, risk, and accounting invariants that JSON Schema cannot express. External strategies use the separate -[strategy protocol v12](../contracts/strategy/v12/README.md). Persistra's host turns protocol +[strategy protocol v13](../contracts/strategy/v13/README.md). Persistra's host turns protocol initialization, marked portfolio contexts, market-slice, fill, order, and rejection events into typed callbacks. Realized weights are available only for positive equity. The retained run manifest binds the strategy identity, executable hash, declared input hashes, transcript hash, @@ -79,14 +79,14 @@ compatibility claim. - **Engine:** `--capabilities` is the authoritative machine-readable surface. The engine must reject unsupported versions and malformed or semantically invalid input before reporting a successful run. -- **Scenario:** Frozen scenario and stream artifacts do not change. The current v14 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v15 contract may receive additive changes only when old valid inputs retain their meaning; breaking changes need a new version. Transitional v3 support remains explicit in `--capabilities`. - **Journal:** A run emits the journal version paired with its accepted scenario. Record ordering, causal references, scenario hashing, terminal completion, and exact accounting remain runtime invariants even when JSON Schema cannot express them. - **Strategy:** Protocol and transcript versions are independent of scenario versions. The current - external boundary is strategy v12; a host must complete its exact initialization, event, + external boundary is strategy v13; a host must complete its exact initialization, event, shutdown, timeout, and rejection lifecycle. - **Persistra:** The required integration gate uses a full Persistra commit and its v3 scenario, journal, and strategy integration tests. Passing that gate claims compatibility only for the diff --git a/docs/scenario.md b/docs/scenario.md index fe9e0a5..64b50cb 100644 --- a/docs/scenario.md +++ b/docs/scenario.md @@ -4,8 +4,8 @@ A replay scenario uses either one strict JSON object or a strict JSON Lines stre 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 v14 demo](../contracts/v14/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v14/scenario.schema.json) provides structural validation. +Use [the v15 demo](../contracts/v15/fixtures/demo.scenario.json) as the canonical complete example. +The [scenario JSON Schema](../contracts/v15/scenario.schema.json) 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. @@ -32,8 +32,8 @@ The batch object and stream header share one domain-construction path and the sa checks. Stream items reuse the batch slice and intent validators directly; no synthetic batch scenario is constructed. -The [stream record JSON Schema](../contracts/v14/scenario-stream.schema.json) validates each line, -and [the v14 stream fixture](../contracts/v14/fixtures/demo.scenario.jsonl) is the canonical example. +The [stream record JSON Schema](../contracts/v15/scenario-stream.schema.json) validates each line, +and [the v15 stream fixture](../contracts/v15/fixtures/demo.scenario.jsonl) 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. @@ -42,7 +42,7 @@ retains current account, order, target, and latest-bar state required by executi | Field | Meaning | |---|---| -| `contract_version` | Required string identifying this file contract; v14 is `"14"` | +| `contract_version` | Required string identifying this file contract; v15 is `"15"` | | `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 | @@ -268,15 +268,25 @@ records `event_at`, `available_at`, `received_at`, and a positive `ingest_sequen 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](../contracts/v14/fixtures/quote-trade.scenario.json) demonstrates passive +[`quote-trade` fixture](../contracts/v15/fixtures/quote-trade.scenario.json) 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](../contracts/v15/fixtures/order-book.scenario.json) 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](../contracts/v14/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v15/journal.schema.json) 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 diff --git a/lib/codec.ml b/lib/codec.ml index 2ec80da..238a9d5 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -389,6 +389,57 @@ let market_event_to_yojson event = string (Market_event.aggressor_side_to_string aggressor_side) ); ]) +let order_book_level_to_yojson level = + `Assoc + [ + ("price", price level.Order_book_event.price); + ("quantity", quantity level.quantity); + ] + +let order_book_event_to_yojson event = + let common = + [ + ("instrument_id", instrument_id event.Order_book_event.instrument_id); + ("event_at", timestamp event.event_at); + ("available_at", timestamp event.available_at); + ("received_at", timestamp event.received_at); + ("ingest_sequence", int64 event.ingest_sequence); + ("book_sequence", int64 event.book_sequence); + ] + in + match event.kind with + | Order_book_event.Snapshot { bids; asks } -> + `Assoc + ((("type", string "snapshot") :: common) + @ [ + ("bids", `List (List.map order_book_level_to_yojson bids)); + ("asks", `List (List.map order_book_level_to_yojson asks)); + ]) + | Set { side; price = value; quantity = size } -> + `Assoc + ((("type", string "set") :: common) + @ [ + ("side", string (Order_book_event.side_to_string side)); + ("price", price value); + ("quantity", quantity size); + ]) + | Delete { side; price = value } -> + `Assoc + ((("type", string "delete") :: common) + @ [ + ("side", string (Order_book_event.side_to_string side)); + ("price", price value); + ]) + | Trade { price = value; quantity = size; aggressor_side } -> + `Assoc + ((("type", string "trade") :: common) + @ [ + ("price", price value); + ("quantity", quantity size); + ( "aggressor_side", + string (Market_event.aggressor_side_to_string aggressor_side) ); + ]) + let versioned_market_slice_to_yojson ~contract_version market_slice = `Assoc [ @@ -406,9 +457,9 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] |> function | `Assoc fields - when List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] -> + when List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] -> let settlement = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ ( "settlement_failures", `List @@ -418,7 +469,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let lifecycle = - if List.mem contract_version [ "14"; "13"; "12" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12" ] then [ ( "lifecycle_events", `List @@ -428,7 +479,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let market_events = - if String.equal contract_version "14" then + if List.mem contract_version [ "15"; "14" ] then [ ( "market_events", `List @@ -437,6 +488,16 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] else [] in + let order_book_events = + if String.equal contract_version "15" then + [ + ( "order_book_events", + `List + (List.map order_book_event_to_yojson + market_slice.Market_slice.order_book_events) ); + ] + else [] + in `Assoc (fields @ [ @@ -449,7 +510,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = (List.map cash_rate_observation_to_yojson market_slice.Market_slice.cash_rate_observations) ); ] - @ settlement @ lifecycle @ market_events) + @ settlement @ lifecycle @ market_events @ order_book_events) | json -> json let market_slice_to_yojson market_slice = @@ -470,6 +531,9 @@ let market_slice_to_yojson_v13 market_slice = let market_slice_to_yojson_v14 market_slice = versioned_market_slice_to_yojson ~contract_version:"14" market_slice +let market_slice_to_yojson_v15 market_slice = + versioned_market_slice_to_yojson ~contract_version:"15" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -573,8 +637,8 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then - order_to_yojson_v8 order + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + then order_to_yojson_v8 order else order_to_yojson order let fill_to_yojson fill = @@ -771,7 +835,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 @@ -780,7 +844,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then position_attribution_to_yojson_v11 else if String.equal contract_version "9" @@ -791,14 +855,15 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ] |> function | `Assoc fields - when List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] -> + when List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9" ] + -> let financing = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -845,7 +910,9 @@ let valuation_to_yojson ~contract_version valuation = | `Assoc fields -> let fields = fields @ [ ("margin", margin_to_yojson valuation.margin) ] in let fields = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + if + List.mem contract_version + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then fields @ [ @@ -987,8 +1054,8 @@ let payload_to_yojson ~contract_version = function ("final_price", price attribution.final_price); ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then - fill_to_yojson_v9 fill + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9" ] + then fill_to_yojson_v9 fill else fill_to_yojson fill | Audit.Settlement_instruction_created instruction | Audit.Settlement_completed instruction diff --git a/lib/codec.mli b/lib/codec.mli index a097970..12ad70c 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -9,6 +9,7 @@ val market_slice_to_yojson_v11 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v12 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v13 : Market_slice.t -> Yojson.Safe.t val market_slice_to_yojson_v14 : Market_slice.t -> Yojson.Safe.t +val market_slice_to_yojson_v15 : Market_slice.t -> Yojson.Safe.t val order_to_yojson : Order.t -> Yojson.Safe.t val order_to_yojson_v8 : Order.t -> Yojson.Safe.t val fill_to_yojson : Fill.t -> Yojson.Safe.t diff --git a/lib/contract.ml b/lib/contract.ml index da5fc9b..f901c21 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "14" -let previous_version = "13" +let version = "15" +let previous_version = "14" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "13"; "12"; "11"; "10"; @@ -19,8 +20,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "12" -let previous_strategy_protocol_version = "11" +let strategy_protocol_version = "13" +let previous_strategy_protocol_version = "12" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -39,6 +40,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "11"; "10"; "9"; "8"; diff --git a/lib/engine.ml b/lib/engine.ml index c5443b0..49af7ba 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -27,6 +27,10 @@ let make_config ~venue_calendars ~contract_version ~risk ~execution_model [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] && Option.is_none (Execution.cost_model execution) then Error "execution model and pricing configuration are incompatible" + else if + String.equal (Execution_model.name execution_model) "order_book_v1" + <> Option.is_some (Execution.book_depth_limit execution) + then Error "execution model and order-book configuration are incompatible" else if max_internal_events <= 0 then Error "maximum internal events must be positive" else if max_internal_events > Resource_limits.internal_events then @@ -70,6 +74,7 @@ let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model let config_v12 = config_v11 let config_v13 = config_v12 let config_v14 = config_v13 +let config_v15 = config_v14 let valid_sha256 value = String.length value = 64 diff --git a/lib/engine.mli b/lib/engine.mli index 4849421..ca85780 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -73,6 +73,17 @@ val config_v14 : max_internal_events:int -> (config, string) result +val config_v15 : + contract_version:string -> + risk:Risk.t -> + venue_calendars:Venue_calendar.t list -> + execution_model:Execution_model.t -> + execution:Execution.t -> + financing:Financing.policy -> + settlement:Settlement.policy -> + max_internal_events:int -> + (config, string) result + module Interactive : sig type t type progress diff --git a/lib/execution.ml b/lib/execution.ml index 1a4fca0..4e60910 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -14,6 +14,7 @@ type t = { participation_bps : int; fee_configuration : fee_configuration; cost_model : cost_model option; + book_depth_limit : int option; } type price_attribution = { @@ -67,6 +68,7 @@ let create ~participation_bps ~fixed_fee ~fee_bps = participation_bps; fee_configuration = Legacy { fixed_fee; fee_bps }; cost_model = None; + book_depth_limit = None; } let create_v2 ~participation_bps ~fee_schedules = @@ -91,6 +93,7 @@ let create_v2 ~participation_bps ~fee_schedules = participation_bps; fee_configuration = Schedules schedules; cost_model = None; + book_depth_limit = None; }) (List.fold_left add (Ok Id.Instrument.Map.empty) fee_schedules) @@ -111,7 +114,16 @@ let create_conservative ~participation_bps ~fee_schedules ~half_spread_bps }) (create_v2 ~participation_bps ~fee_schedules) +let create_order_book ~participation_bps ~fee_schedules ~max_depth_levels = + if max_depth_levels <= 0 || max_depth_levels > 1024 then + Error "order-book depth limit must be between 1 and 1024" + else + Result.map + (fun state -> { state with book_depth_limit = Some max_depth_levels }) + (create_v2 ~participation_bps ~fee_schedules) + let participation_bps state = state.participation_bps +let book_depth_limit state = state.book_depth_limit let fixed_fee state = match state.fee_configuration with @@ -810,6 +822,668 @@ let start_slice_quote_trade state ~instruments ~oms in Ok (make_events events) +type book_state = { + book_sequence : int64; + bids : Order_book_event.level list; + asks : Order_book_event.level list; +} + +type book_view = + | Book_snapshot of book_state + | Book_added of Order_book_event.side * Scalar.Price.t * Scalar.Quantity.t + | Book_reduced of Order_book_event.side * Scalar.Price.t * Scalar.Quantity.t + | Book_trade of + Scalar.Price.t * Scalar.Quantity.t * Market_event.aggressor_side + +let book_level_quantity price levels = + List.find_opt + (fun (level : Order_book_event.level) -> + Scalar.Price.compare level.price price = 0) + levels + |> Option.map (fun level -> level.Order_book_event.quantity) + |> Option.value ~default:Scalar.Quantity.zero + +let sort_book_levels side levels = + List.sort + (fun (left : Order_book_event.level) right -> + let comparison = Scalar.Price.compare left.price right.price in + match side with + | Order_book_event.Bid -> -comparison + | Order_book_event.Ask -> comparison) + levels + +let replace_book_level side price quantity levels = + let level = Order_book_event.level ~price ~quantity |> Result.get_ok in + level + :: List.filter + (fun (existing : Order_book_event.level) -> + Scalar.Price.compare existing.price price <> 0) + levels + |> sort_book_levels side + +let remove_book_level price levels = + List.filter + (fun (level : Order_book_event.level) -> + Scalar.Price.compare level.price price <> 0) + levels + +let consume_book_levels price quantity levels = + let rec consume reversed = function + | [] -> Error "order-book execution level disappeared" + | (level : Order_book_event.level) :: remaining -> + if Scalar.Price.compare level.price price <> 0 then + consume (level :: reversed) remaining + else if Scalar.Quantity.compare quantity level.quantity > 0 then + Error "applied fill quantity exceeds order-book liquidity" + else if Scalar.Quantity.compare quantity level.quantity = 0 then + Ok (List.rev_append reversed remaining) + else + let* remaining_quantity = + Scalar.Quantity.subtract level.quantity quantity + in + let* level = + Order_book_event.level ~price:level.price + ~quantity:remaining_quantity + in + Ok (List.rev_append reversed (level :: remaining)) + in + consume [] levels + +let consume_book_view order price quantity = function + | Book_snapshot book -> ( + match order.Order.request.side with + | Buy -> + Result.map + (fun asks -> Book_snapshot { book with asks }) + (consume_book_levels price quantity book.asks) + | Sell -> + Result.map + (fun bids -> Book_snapshot { book with bids }) + (consume_book_levels price quantity book.bids)) + | Book_added (side, added_price, available) + when Scalar.Price.compare price added_price = 0 -> + if Scalar.Quantity.compare quantity available > 0 then + Error "applied fill quantity exceeds order-book liquidity" + else if Scalar.Quantity.compare quantity available = 0 then + Ok (Book_added (side, added_price, Scalar.Quantity.zero)) + else + let* remaining = Scalar.Quantity.subtract available quantity in + Ok (Book_added (side, added_price, remaining)) + | view -> Ok view + +let valid_book depth_limit book = + List.length book.bids <= depth_limit + && List.length book.asks <= depth_limit + && + match (book.bids, book.asks) with + | bid :: _, ask :: _ -> Scalar.Price.compare bid.price ask.price <= 0 + | _ -> true + +let consume_feed_trade aggressor price quantity book = + let eligible level = + match aggressor with + | Market_event.Buy -> + Scalar.Price.compare level.Order_book_event.price price <= 0 + | Sell -> Scalar.Price.compare level.price price >= 0 + | Unknown -> false + in + let rec consume remaining consumed = function + | levels when Scalar.Quantity.is_zero remaining -> + Ok (List.rev_append consumed levels) + | level :: levels when eligible level -> + if Scalar.Quantity.compare level.quantity remaining <= 0 then + let* remaining = Scalar.Quantity.subtract remaining level.quantity in + consume remaining consumed levels + else + let* quantity = Scalar.Quantity.subtract level.quantity remaining in + let* level = Order_book_event.level ~price:level.price ~quantity in + Ok (List.rev_append consumed (level :: levels)) + | _ -> Error "order-book trade exceeds observable depth" + in + match aggressor with + | Market_event.Buy -> + Result.map + (fun asks -> { book with asks }) + (consume quantity [] book.asks) + | Sell -> + Result.map + (fun bids -> { book with bids }) + (consume quantity [] book.bids) + | Unknown -> Ok book + +let start_slice_order_book state ~instruments ~oms + (market_slice : Market_slice.t) = + let depth_limit = Option.value state.book_depth_limit ~default:0 in + if depth_limit = 0 then Error "order-book execution configuration is required" + else + let instrument_map = + List.fold_left + (fun map instrument -> + Id.Instrument.Map.add instrument.Instrument.id instrument map) + Id.Instrument.Map.empty instruments + in + let validate_event instrument (event : Order_book_event.t) = + let prices, quantities = + match event.kind with + | Snapshot { bids; asks } -> + ( List.map (fun level -> level.Order_book_event.price) (bids @ asks), + List.map + (fun level -> level.Order_book_event.quantity) + (bids @ asks) ) + | Set { price; quantity; _ } -> ([ price ], [ quantity ]) + | Delete { price; _ } -> ([ price ], []) + | Trade { price; quantity; _ } -> ([ price ], [ quantity ]) + in + if + not + (List.for_all + (fun price -> + Scalar.Price.is_multiple price + ~tick:instrument.Instrument.tick_size) + prices) + then Error "order-book price is not aligned to the instrument tick size" + else if + not + (List.for_all + (fun quantity -> + Scalar.Quantity.is_multiple quantity ~lot:instrument.lot_size) + quantities) + then Error "order-book quantity is not aligned to the instrument lot size" + else if + Ptime.compare event.event_at market_slice.start_at < 0 + || Ptime.compare event.event_at market_slice.end_at > 0 + || Ptime.compare event.available_at market_slice.available_at > 0 + || Ptime.compare event.received_at market_slice.received_at > 0 + then Error "order-book event falls outside its observable slice boundary" + else Ok () + in + let prepare (books, prepared) (event : Order_book_event.t) = + let* instrument = + match Id.Instrument.Map.find_opt event.instrument_id instrument_map with + | Some instrument -> Ok instrument + | None -> Error "order-book event refers to an unknown instrument" + in + let* () = validate_event instrument event in + let prior = Id.Instrument.Map.find_opt event.instrument_id books in + let* book, view = + match (prior, event.kind) with + | None, Snapshot { bids; asks } -> + let book = { book_sequence = event.book_sequence; bids; asks } in + if valid_book depth_limit book then Ok (book, Book_snapshot book) + else Error "order-book snapshot exceeds depth or crosses" + | Some _, Snapshot _ -> + Error "order-book bundle contains more than one snapshot" + | None, _ -> Error "order-book bundle must begin with a snapshot" + | Some prior, kind -> ( + if Int64.succ prior.book_sequence <> event.book_sequence then + Error "order-book sequences must be contiguous" + else + let next_sequence book = + { book with book_sequence = event.book_sequence } + in + match kind with + | Set { side; price; quantity } -> + let levels = + match side with + | Bid -> prior.bids + | Order_book_event.Ask -> prior.asks + in + let old_quantity = book_level_quantity price levels in + let levels = replace_book_level side price quantity levels in + let book = + match side with + | Order_book_event.Bid -> + next_sequence { prior with bids = levels } + | Order_book_event.Ask -> + next_sequence { prior with asks = levels } + in + if not (valid_book depth_limit book) then + Error "order-book update exceeds depth or crosses" + else if Scalar.Quantity.compare quantity old_quantity > 0 then + let* added = + Scalar.Quantity.subtract quantity old_quantity + in + Ok (book, Book_added (side, price, added)) + else + let* removed = + Scalar.Quantity.subtract old_quantity quantity + in + Ok (book, Book_reduced (side, price, removed)) + | Delete { side; price } -> + let levels = + match side with + | Bid -> prior.bids + | Order_book_event.Ask -> prior.asks + in + let old_quantity = book_level_quantity price levels in + if Scalar.Quantity.is_zero old_quantity then + Error "order-book delete refers to a missing level" + else + let levels = remove_book_level price levels in + let book = + match side with + | Order_book_event.Bid -> + next_sequence { prior with bids = levels } + | Order_book_event.Ask -> + next_sequence { prior with asks = levels } + in + Ok (book, Book_reduced (side, price, old_quantity)) + | Trade { price; quantity; aggressor_side } -> + let* book = + consume_feed_trade aggressor_side price quantity prior + in + Ok + ( next_sequence book, + Book_trade (price, quantity, aggressor_side) ) + | Snapshot _ -> assert false) + in + Ok + ( Id.Instrument.Map.add event.instrument_id book books, + (event, instrument, view) :: prepared ) + in + let* books, reversed = + List.fold_left + (fun result event -> + Result.bind result (fun state -> prepare state event)) + (Ok (Id.Instrument.Map.empty, [])) + market_slice.order_book_events + in + let expected = + List.map (fun instrument -> instrument.Instrument.id) instruments + |> Id.Instrument.Set.of_list + in + let observed = + Id.Instrument.Map.fold + (fun instrument_id _ ids -> Id.Instrument.Set.add instrument_id ids) + books Id.Instrument.Set.empty + in + if not (Id.Instrument.Set.equal expected observed) then + Error "order-book snapshots must cover every configured instrument" + else + let events = List.rev reversed in + let eligible = + Oms.active_orders oms + |> List.filter (fun order -> + Int64.compare order.Order.eligible_after_slice_sequence + market_slice.slice_sequence + < 0 + && Ptime.compare order.created_at market_slice.start_at <= 0) + |> List.sort compare_execution_order + in + let order_ids = List.map (fun order -> order.Order.id) eligible in + let market_ioc_orders = + List.filter_map + (fun order -> + if Order.is_ioc order && not (Order.is_dormant_stop order) then + Some order.Order.id + else None) + eligible + in + let queue_for_snapshot queues instrument book = + let rec build prior queues = function + | [] -> Ok queues + | order :: remaining -> ( + match Order.effective_kind order with + | Some (Order.Limit limit) + when Id.Instrument.equal order.request.instrument_id + instrument.Instrument.id -> + let opposite = + match order.request.side with + | Buy -> book.asks + | Sell -> book.bids + in + let marketable = + match opposite with + | [] -> false + | best :: _ -> ( + match order.request.side with + | Buy -> Scalar.Price.compare best.price limit <= 0 + | Sell -> Scalar.Price.compare best.price limit >= 0) + in + if marketable then build (order :: prior) queues remaining + else + let same_side = + match order.request.side with + | Buy -> book.bids + | Sell -> book.asks + in + let external_quantity = + book_level_quantity limit same_side + in + let* ahead = + List.fold_left + (fun result earlier -> + let* ahead = result in + match Order.effective_kind earlier with + | Some (Order.Limit earlier_limit) + when earlier.request.side = order.request.side + && Id.Instrument.equal + earlier.request.instrument_id + order.request.instrument_id + && Scalar.Price.compare earlier_limit limit = 0 + -> + Scalar.Quantity.add ahead + (Order.remaining_quantity earlier) + | _ -> Ok ahead) + (Ok external_quantity) prior + in + build (order :: prior) + (Id.Order.Map.add order.id ahead queues) + remaining + | _ -> build (order :: prior) queues remaining) + in + build [] queues eligible + in + let order_matches_level order side price = + match Order.effective_kind order with + | Some Order.Market -> + (order.request.side = Buy && side = Order_book_event.Ask) + || (order.request.side = Sell && side = Order_book_event.Bid) + | Some (Limit limit) -> + order.request.side = Buy + && side = Order_book_event.Ask + && Scalar.Price.compare price limit <= 0 + || order.request.side = Sell + && side = Order_book_event.Bid + && Scalar.Price.compare price limit >= 0 + | _ -> false + in + let levels_for_order order view = + match view with + | Book_snapshot book -> + let levels = + match order.Order.request.side with + | Buy -> book.asks + | Sell -> book.bids + in + List.filter + (fun level -> + order_matches_level order + (match order.request.side with + | Buy -> Order_book_event.Ask + | Sell -> Order_book_event.Bid) + level.Order_book_event.price) + levels + | Book_added (side, price, quantity) + when order_matches_level order side price + && not (Scalar.Quantity.is_zero quantity) -> + [ Order_book_event.level ~price ~quantity |> Result.get_ok ] + | _ -> [] + in + let reduce_queue queues instrument_id side price removed = + Id.Order.Map.mapi + (fun order_id ahead -> + match Oms.find oms order_id with + | Some order + when Id.Instrument.equal order.request.instrument_id instrument_id + && (match order.request.side with + | Buy -> side = Order_book_event.Bid + | Sell -> side = Order_book_event.Ask) + && + match Order.effective_kind order with + | Some (Limit limit) -> Scalar.Price.compare limit price = 0 + | _ -> false -> + if Scalar.Quantity.compare removed ahead >= 0 then + Scalar.Quantity.zero + else Scalar.Quantity.subtract ahead removed |> Result.get_ok + | _ -> ahead) + queues + in + let trade_allowances queues instrument_id price quantity aggressor = + Id.Order.Map.fold + (fun order_id ahead (queues, allowances) -> + match Oms.find oms order_id with + | Some order + when Id.Instrument.equal order.request.instrument_id instrument_id + && (match (order.request.side, aggressor) with + | Buy, Market_event.Sell | Sell, Buy -> true + | _ -> false) + && + match Order.effective_kind order with + | Some (Limit limit) -> ( + match order.request.side with + | Buy -> Scalar.Price.compare price limit <= 0 + | Sell -> Scalar.Price.compare price limit >= 0) + | _ -> false -> + let next_ahead = + if Scalar.Quantity.compare quantity ahead >= 0 then + Scalar.Quantity.zero + else Scalar.Quantity.subtract ahead quantity |> Result.get_ok + in + let through = + if Scalar.Quantity.compare quantity ahead <= 0 then + Scalar.Quantity.zero + else Scalar.Quantity.subtract quantity ahead |> Result.get_ok + in + ( Id.Order.Map.add order_id next_ahead queues, + Id.Order.Map.add order_id through allowances ) + | _ -> (queues, allowances)) + queues + (queues, Id.Order.Map.empty) + in + let rec make_events queues = function + | [] -> Ok (cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders))) + | ((event : Order_book_event.t), instrument, view) :: remaining_events + -> + let* queues, allowances = + match view with + | Book_snapshot book -> + Result.map + (fun queues -> (queues, Id.Order.Map.empty)) + (queue_for_snapshot queues instrument book) + | Book_reduced (side, price, removed) -> + Ok + ( reduce_queue queues event.instrument_id side price removed, + Id.Order.Map.empty ) + | Book_trade (price, quantity, aggressor) -> + Ok + (trade_allowances queues event.instrument_id price quantity + aggressor) + | Book_added _ -> Ok (queues, Id.Order.Map.empty) + in + Ok + (make_orders queues allowances event instrument view order_ids + remaining_events) + and make_orders queues allowances event instrument view remaining + remaining_events = + Cursor + (fun current_oms -> + match remaining with + | [] -> + let* cursor = make_events queues remaining_events in + let (Cursor next) = cursor in + next current_oms + | order_id :: remaining_orders -> ( + match Oms.find current_oms order_id with + | None -> + Error "eligible order disappeared during order-book replay" + | Some order when not (Order.is_active order) -> + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + | Some order + when not + (Id.Instrument.equal order.request.instrument_id + event.Order_book_event.instrument_id) -> + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + | Some order when Order.is_dormant_stop order -> + let observed = + match view with + | Book_snapshot book -> ( + match order.request.side with + | Buy -> ( + match book.asks with + | level :: _ -> Some level.Order_book_event.price + | [] -> None) + | Sell -> ( + match book.bids with + | level :: _ -> Some level.Order_book_event.price + | [] -> None)) + | Book_added (_, price, _) + | Book_reduced (_, price, _) + | Book_trade (price, _, _) -> + Some price + in + let triggered = + match + (order.request.kind, order.request.side, observed) + with + | ( ( Stop trigger + | Stop_limit { trigger_price = trigger; _ } ), + Buy, + Some price ) -> + Scalar.Price.compare price trigger >= 0 + | ( ( Stop trigger + | Stop_limit { trigger_price = trigger; _ } ), + Sell, + Some price ) -> + Scalar.Price.compare price trigger <= 0 + | _ -> false + in + let continuation = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + if triggered then + Ok + (Triggered + ( order.id, + event.event_at, + market_slice.slice_sequence, + continuation )) + else + let (Cursor next) = continuation in + next current_oms + | Some order -> ( + let levels = levels_for_order order view in + let passive = Id.Order.Map.find_opt order.id allowances in + let* fok_capacity = + List.fold_left + (fun result (level : Order_book_event.level) -> + let* total = result in + let* capacity = + event_capacity state instrument level.quantity + in + Scalar.Quantity.add total capacity) + (Ok Scalar.Quantity.zero) levels + in + let opportunity = + match (levels, passive, view) with + | level :: _, _, _ -> + Some + ( level.price, + level.quantity, + Fee_schedule.Taker, + true ) + | [], Some quantity, Book_trade (price, _, _) -> + Some (price, quantity, Fee_schedule.Maker, false) + | _ -> None + in + match opportunity with + | None -> + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + | Some (price, available, fee_liquidity, repeat_order) -> + let* capacity = + event_capacity state instrument available + in + let quantity = + Scalar.Quantity.minimum capacity + (Order.remaining_quantity order) + in + if + Scalar.Quantity.is_zero quantity + || Order.is_fok order + && Scalar.Quantity.compare + (if levels = [] then capacity + else fok_capacity) + (Order.remaining_quantity order) + < 0 + then + let (Cursor next) = + make_orders queues allowances event instrument view + remaining_orders remaining_events + in + next current_oms + else + let* notional = + Scalar.Money.notional price quantity + in + let* fee_components, fee = + calculate_fee state ~instrument ~notional ~quantity + ~liquidity:fee_liquidity + ~fx_rates: + (List.map + (fun mark -> + (mark.Market_slice.currency, mark.rate)) + market_slice.fx_rates) + in + let proposed = + { + order_id = order.id; + quantity; + price; + fee; + fee_components; + liquidity = fee_liquidity; + executed_at = event.event_at; + price_attribution = None; + } + in + let continue applied_quantity = + if + Scalar.Quantity.compare applied_quantity quantity + > 0 + then + Error + "applied fill quantity exceeds order-book \ + liquidity" + else if + Scalar.Quantity.compare applied_quantity + Scalar.Quantity.zero + < 0 + then + Error "applied fill quantity must be nonnegative" + else if + not + (Scalar.Quantity.is_multiple applied_quantity + ~lot:instrument.Instrument.lot_size) + then + Error + "applied fill quantity is not aligned to the \ + instrument lot size" + else + let* next_view = + if repeat_order then + consume_book_view order price applied_quantity + view + else Ok view + in + let next_orders = + if + repeat_order + && not + (Scalar.Quantity.is_zero applied_quantity) + then order_id :: remaining_orders + else remaining_orders + in + Ok + (make_orders queues allowances event instrument + next_view next_orders remaining_events) + in + Ok (Proposed (proposed, continue))))) + in + make_events Id.Order.Map.empty events + let finished market_ioc_orders = cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders)) diff --git a/lib/execution.mli b/lib/execution.mli index 8ba8765..da67d1c 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -62,7 +62,14 @@ val create_conservative : missing_volume_policy:missing_volume_policy -> (t, string) result +val create_order_book : + participation_bps:int -> + fee_schedules:Fee_schedule.t list -> + max_depth_levels:int -> + (t, string) result + val participation_bps : t -> int +val book_depth_limit : t -> int option val fixed_fee : t -> Scalar.Money.t val fee_bps : t -> int val fee_schedules : t -> Fee_schedule.t list @@ -107,6 +114,13 @@ val start_slice_quote_trade : Market_slice.t -> (cursor, string) result +val start_slice_order_book : + t -> + instruments:Instrument.t list -> + oms:Oms.t -> + Market_slice.t -> + (cursor, string) result + val finished : Id.Order.t list -> cursor (** Build a cursor that immediately finishes. This supports execution models that intentionally produce no proposals. *) diff --git a/lib/execution_model.ml b/lib/execution_model.ml index ddd6154..eef0033 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -42,6 +42,11 @@ module Quote_trade_v1 = struct let start_slice = Execution.start_slice_quote_trade end +module Order_book_v1 = struct + let name = "order_book_v1" + let start_slice = Execution.start_slice_order_book +end + let of_module model = model let name (module Model : S) = Model.name @@ -51,6 +56,7 @@ let builtins : t list = (module Completed_bar_next_open_v1); (module Completed_bar_adverse_touch_v1); (module Quote_trade_v1); + (module Order_book_v1); ] let supported = List.map name builtins @@ -60,7 +66,7 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = [ "version"; "participation_bps"; "fixed_fee"; "fee_bps" ]; @@ -81,7 +87,7 @@ let conservative_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "14"; "13" ]; + scenario_contract_versions = [ "15"; "14"; "13" ]; required_fields = [ "version"; @@ -110,7 +116,7 @@ let quote_trade_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "14" ]; + scenario_contract_versions = [ "15"; "14" ]; required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; legacy_required_fields = []; supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; @@ -128,12 +134,39 @@ let quote_trade_contract = ]; } +let order_book_contract = + { + version = "1"; + previous_versions = []; + scenario_contract_versions = [ "15" ]; + required_fields = + [ "version"; "participation_bps"; "fee_schedules"; "max_depth_levels" ]; + legacy_required_fields = []; + supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; + data_requirements = + [ + "slice_open_level_two_snapshot"; + "contiguous_absolute_level_updates"; + "aggressor_classified_depth_consuming_trades"; + "completed_bars_for_valuation"; + ]; + limits = + `Assoc + [ + ( "participation_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ( "max_depth_levels", + `Assoc [ ("minimum", `Int 1); ("maximum", `Int 1024) ] ); + ]; + } + let configuration_contract model = match name model with | "completed_bar_v1" -> completed_bar_v1_contract | "completed_bar_next_open_v1" | "completed_bar_adverse_touch_v1" -> conservative_contract | "quote_trade_v1" -> quote_trade_contract + | "order_book_v1" -> order_book_contract | unsupported -> invalid_arg (Printf.sprintf "execution model %S has no configuration contract" diff --git a/lib/external_replay.ml b/lib/external_replay.ml index 23c422c..1c95491 100644 --- a/lib/external_replay.ml +++ b/lib/external_replay.ml @@ -105,7 +105,11 @@ let create_runner ~contract_version ~run_id ~scenario_sha256 ~risk Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events | Some financing, Some settlement -> - if String.equal contract_version "14" then + if String.equal contract_version "15" then + Engine.config_v15 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "14" then Engine.config_v14 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/market_slice.ml b/lib/market_slice.ml index e9a1613..130efe0 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -8,6 +8,7 @@ type t = { received_at : Ptime.t; bars : Bar.t list; market_events : Market_event.t list; + order_book_events : Order_book_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -32,10 +33,10 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at +let create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events - ~market_events = + ~market_events ~order_book_events = if Int64.compare slice_sequence 0L <= 0 then Error "market slice sequence must be positive" else if Ptime.compare start_at end_at >= 0 then @@ -134,6 +135,15 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at < 0 && ordered_events remaining in + let rec ordered_book_events = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + Order_book_event.compare_replay_order left right < 0 + && Int64.compare left.Order_book_event.ingest_sequence + right.Order_book_event.ingest_sequence + < 0 + && ordered_book_events remaining + in if not (unique bars) then Error "market slice must contain one bar per instrument" else if fx_rates = [] then Error "market slice must contain FX rates" @@ -153,6 +163,10 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at Error "market events must be strictly ordered by availability, receipt, and \ ingest sequence" + else if not (ordered_book_events order_book_events) then + Error + "order-book events must be strictly ordered by availability, receipt, \ + and ingest sequence" else Ok { @@ -163,6 +177,7 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at received_at; bars; market_events; + order_book_events; fx_rates; corporate_actions; lifecycle_events; @@ -171,12 +186,21 @@ let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } +let create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events + ~market_events = + create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations + ~settlement_failures ~lifecycle_events ~market_events ~order_book_events:[] + let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events = - create_v14 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars + create_v15 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures ~lifecycle_events ~market_events:[] + ~order_book_events:[] let create_v13 = create_v12 @@ -216,10 +240,11 @@ let compare_replay_order left right = let pp formatter state = Format.fprintf formatter - "slice[%Ld] bars=%d events=%d fx=%d actions=%d lifecycle=%d borrow=%d \ - cash_rates=%d failures=%d" + "slice[%Ld] bars=%d events=%d book_events=%d fx=%d actions=%d lifecycle=%d \ + borrow=%d cash_rates=%d failures=%d" state.slice_sequence (List.length state.bars) (List.length state.market_events) + (List.length state.order_book_events) (List.length state.fx_rates) (List.length state.corporate_actions) (List.length state.lifecycle_events) diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 5785f03..a1e0ded 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -13,6 +13,7 @@ type t = private { received_at : Ptime.t; bars : Bar.t list; market_events : Market_event.t list; + order_book_events : Order_book_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -105,6 +106,23 @@ val create_v14 : market_events:Market_event.t list -> (t, string) result +val create_v15 : + slice_sequence:int64 -> + start_at:Ptime.t -> + end_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + bars:Bar.t list -> + fx_rates:fx_mark list -> + corporate_actions:Corporate_action.t list -> + borrow_observations:Financing.borrow_observation list -> + cash_rate_observations:Financing.cash_rate_observation list -> + settlement_failures:Settlement.failure list -> + lifecycle_events:Instrument_lifecycle.event list -> + market_events:Market_event.t list -> + order_book_events:Order_book_event.t list -> + (t, string) result + val bar : t -> Id.Instrument.t -> Bar.t option val fx_rate : t -> string -> Scalar.Price.t option val compare_replay_order : t -> t -> int diff --git a/lib/order_book_event.ml b/lib/order_book_event.ml new file mode 100644 index 0000000..701c837 --- /dev/null +++ b/lib/order_book_event.ml @@ -0,0 +1,132 @@ +type side = Bid | Ask +type level = { price : Scalar.Price.t; quantity : Scalar.Quantity.t } + +type kind = + | Snapshot of { bids : level list; asks : level list } + | Set of { side : side; price : Scalar.Price.t; quantity : Scalar.Quantity.t } + | Delete of { side : side; price : Scalar.Price.t } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : Market_event.aggressor_side; + } + +type t = { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + book_sequence : int64; + kind : kind; +} + +let level ~price ~quantity = + if Scalar.Quantity.is_zero quantity then + Error "order-book level quantity must be positive" + else Ok { price; quantity } + +let validate_common ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence = + if Int64.compare ingest_sequence 0L <= 0 then + Error "order-book ingest sequence must be positive" + else if Int64.compare book_sequence 0L <= 0 then + Error "order-book sequence must be positive" + else if Ptime.compare available_at event_at < 0 then + Error "order-book availability must not precede event time" + else if Ptime.compare received_at available_at < 0 then + Error "order-book receipt must not precede availability" + else Ok () + +let ordered_levels side levels = + let rec ordered = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + let comparison = Scalar.Price.compare left.price right.price in + (match side with Bid -> comparison > 0 | Ask -> comparison < 0) + && ordered remaining + in + ordered levels + +let snapshot ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence ~bids ~asks = + let ( let* ) = Result.bind in + let* () = + validate_common ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + in + if bids = [] || asks = [] then + Error "order-book snapshot must contain bid and ask depth" + else if not (ordered_levels Bid bids && ordered_levels Ask asks) then + Error "order-book snapshot levels must be unique and price ordered" + else if Scalar.Price.compare (List.hd bids).price (List.hd asks).price > 0 + then Error "crossed order-book snapshot is invalid" + else + Ok + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + book_sequence; + kind = Snapshot { bids; asks }; + } + +let create_change kind ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence = + Result.map + (fun () -> + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + book_sequence; + kind; + }) + (validate_common ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence) + +let set ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence ~side ~price ~quantity = + if Scalar.Quantity.is_zero quantity then + Error "order-book set quantity must be positive" + else + create_change + (Set { side; price; quantity }) + ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + +let delete ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence ~side ~price = + create_change + (Delete { side; price }) + ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + +let trade ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence ~price ~quantity ~aggressor_side = + if Scalar.Quantity.is_zero quantity then + Error "order-book trade quantity must be positive" + else + create_change + (Trade { price; quantity; aggressor_side }) + ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~book_sequence + +let compare_replay_order left right = + let availability = Ptime.compare left.available_at right.available_at in + if availability <> 0 then availability + else + let receipt = Ptime.compare left.received_at right.received_at in + if receipt <> 0 then receipt + else Int64.compare left.ingest_sequence right.ingest_sequence + +let side_to_string = function Bid -> "bid" | Ask -> "ask" + +let side_of_string = function + | "bid" -> Ok Bid + | "ask" -> Ok Ask + | _ -> Error "order-book side must be bid or ask" diff --git a/lib/order_book_event.mli b/lib/order_book_event.mli new file mode 100644 index 0000000..4fc6eb3 --- /dev/null +++ b/lib/order_book_event.mli @@ -0,0 +1,77 @@ +(** Causally ordered level-two order-book observations. *) + +type side = Bid | Ask +type level = private { price : Scalar.Price.t; quantity : Scalar.Quantity.t } + +type kind = + | Snapshot of { bids : level list; asks : level list } + | Set of { side : side; price : Scalar.Price.t; quantity : Scalar.Quantity.t } + | Delete of { side : side; price : Scalar.Price.t } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : Market_event.aggressor_side; + } + +type t = private { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + book_sequence : int64; + kind : kind; +} + +val level : + price:Scalar.Price.t -> quantity:Scalar.Quantity.t -> (level, string) result + +val snapshot : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + bids:level list -> + asks:level list -> + (t, string) result + +val set : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + side:side -> + price:Scalar.Price.t -> + quantity:Scalar.Quantity.t -> + (t, string) result + +val delete : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + side:side -> + price:Scalar.Price.t -> + (t, string) result + +val trade : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + book_sequence:int64 -> + price:Scalar.Price.t -> + quantity:Scalar.Quantity.t -> + aggressor_side:Market_event.aggressor_side -> + (t, string) result + +val compare_replay_order : t -> t -> int +val side_to_string : side -> string +val side_of_string : string -> (side, string) result diff --git a/lib/replay.ml b/lib/replay.ml index 6807cda..3378072 100644 --- a/lib/replay.ml +++ b/lib/replay.ml @@ -78,7 +78,11 @@ let engine_config ~contract_version ~risk ~venue_calendars ~execution_model Engine.config_v10 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~max_internal_events | Some financing, Some settlement -> - if String.equal contract_version "14" then + if String.equal contract_version "15" then + Engine.config_v15 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "14" then Engine.config_v14 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/scenario.ml b/lib/scenario.ml index dcf86bc..81877d0 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -554,7 +554,9 @@ let parse_v7_risk base_currency instruments json = ~max_gross_exposure ~max_leverage ~short_borrow_bps let parse_risk ~contract_version base_currency instruments json = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + if + List.mem contract_version + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -687,6 +689,18 @@ let parse_execution_v2 instruments fields = in Execution.create_v2 ~participation_bps ~fee_schedules:schedules +let parse_order_book_execution instruments fields = + let* participation_bps, fee_schedules = + parse_execution_common instruments fields + in + let* max_depth_levels = + Result.bind + (field fields "max_depth_levels") + (integer ~name:"max_depth_levels") + in + Execution.create_order_book ~participation_bps ~fee_schedules + ~max_depth_levels + let parse_conservative_execution instruments fields = let* participation_bps, fee_schedules = parse_execution_common instruments fields @@ -803,6 +817,8 @@ let parse_versioned_execution ~contract_version ~instruments json = List.mem model_name [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] then parse_conservative_execution instruments configuration + else if String.equal model_name "order_book_v1" then + parse_order_book_execution instruments configuration else if String.equal model_name "quote_trade_v1" || String.equal version "2" then parse_execution_v2 instruments configuration @@ -813,7 +829,7 @@ let parse_versioned_execution ~contract_version ~instruments json = let parse_execution ~contract_version ~instruments json = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -863,7 +879,7 @@ let parse_portfolio_intent ~name ~parse_target make json = let parse_submit_intent ~contract_version json = let versioned = - List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" @@ -1698,24 +1714,161 @@ let parse_market_event json = ~ingest_sequence ~price ~quantity ~aggressor_side | _ -> assert false +let parse_order_book_level json = + let* fields = + object_fields ~name:"order-book level" ~expected:[ "price"; "quantity" ] + json + in + let* price = + Result.bind (field fields "price") (parse_price ~name:"book level price") + in + let* quantity = + Result.bind (field fields "quantity") + (parse_quantity ~name:"book level quantity") + in + Order_book_event.level ~price ~quantity + +let parse_order_book_event json = + let* loose_fields = + match json with + | `Assoc fields -> Ok fields + | _ -> Error "order-book event must be a JSON object" + in + let* type_name = + Result.bind + (field loose_fields "type") + (string ~name:"order-book event type") + in + let common = + [ + "type"; + "instrument_id"; + "event_at"; + "available_at"; + "received_at"; + "ingest_sequence"; + "book_sequence"; + ] + in + let specific = + match type_name with + | "snapshot" -> [ "bids"; "asks" ] + | "set" -> [ "side"; "price"; "quantity" ] + | "delete" -> [ "side"; "price" ] + | "trade" -> [ "price"; "quantity"; "aggressor_side" ] + | _ -> [] + in + let* () = + if specific = [] then + Error "order-book event type must be snapshot, set, delete, or trade" + else Ok () + in + let* fields = + object_fields + ~name:(type_name ^ " order-book event") + ~expected:(common @ specific) json + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"order-book instrument_id") + in + let* event_at = + Result.bind (field fields "event_at") + (parse_timestamp ~name:"order-book event_at") + in + let* available_at = + Result.bind + (field fields "available_at") + (parse_timestamp ~name:"order-book available_at") + in + let* received_at = + Result.bind + (field fields "received_at") + (parse_timestamp ~name:"order-book received_at") + in + let* ingest_sequence = + Result.bind + (field fields "ingest_sequence") + (parse_int64 ~name:"order-book ingest_sequence") + in + let* book_sequence = + Result.bind + (field fields "book_sequence") + (parse_int64 ~name:"order-book book_sequence") + in + let side () = + let* value = + Result.bind (field fields "side") (string ~name:"order-book side") + in + Order_book_event.side_of_string value + in + let price () = + Result.bind (field fields "price") (parse_price ~name:"order-book price") + in + let quantity () = + Result.bind (field fields "quantity") + (parse_quantity ~name:"order-book quantity") + in + match type_name with + | "snapshot" -> + let* bids_json = + Result.bind (field fields "bids") (list ~name:"order-book bids") + in + let* asks_json = + Result.bind (field fields "asks") (list ~name:"order-book asks") + in + let* bids = map_list parse_order_book_level bids_json in + let* asks = map_list parse_order_book_level asks_json in + Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence ~book_sequence ~bids ~asks + | "set" -> + let* side = side () in + let* price = price () in + let* quantity = quantity () in + Order_book_event.set ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence ~side ~price ~quantity + | "delete" -> + let* side = side () in + let* price = price () in + Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence ~book_sequence ~side ~price + | "trade" -> + let* price = price () in + let* quantity = quantity () in + let* aggressor_name = + Result.bind + (field fields "aggressor_side") + (string ~name:"order-book aggressor_side") + in + let* aggressor_side = + Market_event.aggressor_side_of_string aggressor_name + in + Order_book_event.trade ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~book_sequence ~price ~quantity ~aggressor_side + | _ -> assert false + let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ "settlement_failures" ] else [] in let lifecycle_fields = - if List.mem contract_version [ "14"; "13"; "12" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12" ] then [ "lifecycle_events" ] else [] in let market_event_fields = - if String.equal contract_version "14" then [ "market_events" ] else [] + if List.mem contract_version [ "15"; "14" ] then [ "market_events" ] else [] + in + let order_book_event_fields = + if String.equal contract_version "15" then [ "order_book_events" ] else [] in let* fields = object_fields ~name:"market slice" @@ -1731,7 +1884,7 @@ let parse_slice ~contract_version json = "corporate_actions"; ] @ financing_fields @ settlement_fields @ lifecycle_fields - @ market_event_fields) + @ market_event_fields @ order_book_event_fields) json in let* sequence_json = field fields "slice_sequence" in @@ -1753,7 +1906,7 @@ let parse_slice ~contract_version json = let* actions_json = field fields "corporate_actions" in let* actions_json = list ~name:"corporate_actions" actions_json in let* corporate_actions = map_list parse_corporate_action actions_json in - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1768,7 +1921,7 @@ let parse_slice ~contract_version json = let* cash_rate_observations = map_list parse_cash_rate_observation cash_json in - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1777,24 +1930,38 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - if List.mem contract_version [ "14"; "13"; "12" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12" ] then let* lifecycle_json = Result.bind (field fields "lifecycle_events") (list ~name:"lifecycle_events") in let* lifecycle_events = map_list parse_lifecycle_event lifecycle_json in - if String.equal contract_version "14" then + if List.mem contract_version [ "15"; "14" ] then let* events_json = Result.bind (field fields "market_events") (list ~name:"market_events") in let* market_events = map_list parse_market_event events_json in - Market_slice.create_v14 ~slice_sequence ~start_at ~end_at - ~available_at ~received_at ~bars ~fx_rates ~corporate_actions - ~borrow_observations ~cash_rate_observations ~settlement_failures - ~lifecycle_events ~market_events + if String.equal contract_version "15" then + let* book_events_json = + Result.bind + (field fields "order_book_events") + (list ~name:"order_book_events") + in + let* order_book_events = + map_list parse_order_book_event book_events_json + in + Market_slice.create_v15 ~slice_sequence ~start_at ~end_at + ~available_at ~received_at ~bars ~fx_rates ~corporate_actions + ~borrow_observations ~cash_rate_observations ~settlement_failures + ~lifecycle_events ~market_events ~order_book_events + else + Market_slice.create_v14 ~slice_sequence ~start_at ~end_at + ~available_at ~received_at ~bars ~fx_rates ~corporate_actions + ~borrow_observations ~cash_rate_observations ~settlement_failures + ~lifecycle_events ~market_events else let create = if String.equal contract_version "13" then Market_slice.create_v13 @@ -1848,7 +2015,7 @@ let construct_header ~root ~contract_path ~contract_version let* initial_cash, initial_portfolio = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state @@ -1918,7 +2085,7 @@ let construct_header ~root ~contract_path ~contract_version | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then Some financing else None in diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index e85661d..e324722 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -72,7 +72,7 @@ let common ~root ~contract_version fields = let initial_field = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -81,19 +81,19 @@ let common ~root ~contract_version fields = let venue_calendars = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then List.assoc_opt "venue_calendars" fields else None in let* risk = field ~root fields "risk" in let* execution = field ~root fields "execution" in let financing = - if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] then List.assoc_opt "financing" fields else None in let settlement = - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -126,14 +126,14 @@ let batch json = let calendar_fields = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -154,11 +154,11 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then - [ "financing" ] + @ (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] + then [ "financing" ] else []) @ - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ "settlement" ] else []) json @@ -174,14 +174,14 @@ let stream_header ~contract_version json = let calendar_fields = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -199,11 +199,11 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then - [ "financing" ] + @ (if List.mem contract_version [ "15"; "14"; "13"; "12"; "11"; "10" ] + then [ "financing" ] else []) @ - if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + if List.mem contract_version [ "15"; "14"; "13"; "12"; "11" ] then [ "settlement" ] else []) json diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 45032db..d09f3b7 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -50,7 +50,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash @@ -71,7 +71,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -91,7 +91,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments (child root (if List.mem contract_version - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 120f8cb..ed1c9c5 100644 --- a/lib/strategy_protocol.ml +++ b/lib/strategy_protocol.ml @@ -103,7 +103,7 @@ let group_kind_to_string = function let nullable render = Option.fold ~none:`Null ~some:render let modern_protocol protocol_version = - List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -253,7 +253,7 @@ let execution_to_yojson ~protocol_version model execution = ] in if - List.mem protocol_version [ "12"; "11" ] + List.mem protocol_version [ "13"; "12"; "11" ] && List.mem (Execution_model.name model) [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] @@ -291,7 +291,7 @@ let execution_to_yojson ~protocol_version model execution = ] ); ] else if - String.equal protocol_version "12" + List.mem protocol_version [ "13"; "12" ] && String.equal (Execution_model.name model) "quote_trade_v1" then `Assoc @@ -308,7 +308,28 @@ let execution_to_yojson ~protocol_version model execution = (Execution.fee_schedules execution)) ); ] ); ] - else if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] then + else if + String.equal protocol_version "13" + && String.equal (Execution_model.name model) "order_book_v1" + then + `Assoc + [ + ("model", string (Execution_model.name model)); + ( "configuration", + `Assoc + [ + ("version", string "1"); + ("participation_bps", `Int (Execution.participation_bps execution)); + ( "fee_schedules", + `List + (List.map fee_schedule_to_yojson + (Execution.fee_schedules execution)) ); + ( "max_depth_levels", + `Int (Option.get (Execution.book_depth_limit execution)) ); + ] ); + ] + else if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + then `Assoc [ ("model", string (Execution_model.name model)); @@ -347,6 +368,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "15" -> "13" | "14" -> "12" | "13" -> "11" | "12" -> "10" @@ -395,7 +417,8 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -408,14 +431,15 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then + (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8" ] + then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); - (if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then + (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -449,14 +473,14 @@ let cash_attribution_to_yojson ~protocol_version ("fx_rate", price balance.fx_rate); ("base_value", money balance.base_value); ] - @ (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then + @ (if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then + if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -476,7 +500,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then + if List.mem protocol_version [ "13"; "12"; "11"; "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -561,7 +585,7 @@ let context_to_yojson ~protocol_version context = (List.map (if List.mem protocol_version - [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -574,7 +598,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if String.equal protocol_version "12" then + if String.equal protocol_version "13" then + Codec.market_slice_to_yojson_v15 market_slice + else if String.equal protocol_version "12" then Codec.market_slice_to_yojson_v14 market_slice else if String.equal protocol_version "11" then Codec.market_slice_to_yojson_v13 market_slice @@ -591,7 +617,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] + if + List.mem protocol_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -601,7 +629,8 @@ let event_to_yojson ~protocol_version = function ("type", string "order_updated"); ( "order", if - List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + List.mem protocol_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -689,7 +718,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "12" then "14" + (if String.equal protocol_version "13" then "15" + else if String.equal protocol_version "12" then "14" else if String.equal protocol_version "11" then "13" else if String.equal protocol_version "10" then "12" else if String.equal protocol_version "9" then "11" diff --git a/mkdocs.yml b/mkdocs.yml index 81af8aa..2f04317 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v14: contracts/v14/README.md + - Current v15: contracts/v15/README.md - Transitional v5: contracts/v5/README.md - Transitional v4: contracts/v4/README.md - Transitional v3: contracts/v3/README.md - Frozen v2: contracts/v2/README.md - Historical v1: contracts/v1/README.md - External strategy: - - Current v12: contracts/strategy/v12/README.md + - Current v13: contracts/strategy/v13/README.md - Historical v3: contracts/strategy/v3/README.md - Historical v2: contracts/strategy/v2/README.md - Historical v1: contracts/strategy/v1/README.md diff --git a/scripts/check-deterministic-journals b/scripts/check-deterministic-journals index 5966154..74a05c0 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -110,3 +110,19 @@ compare_journal \ v14-quote-trade \ contracts/v14/fixtures/quote-trade.scenario.json \ contracts/v14/fixtures/quote-trade.journal.jsonl +compare_journal \ + v15-demo \ + contracts/v15/fixtures/demo.scenario.json \ + contracts/v15/fixtures/demo.journal.jsonl +compare_journal \ + v15-fill-clipped \ + contracts/v15/fixtures/fill-clipped.scenario.json \ + contracts/v15/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v15-quote-trade \ + contracts/v15/fixtures/quote-trade.scenario.json \ + contracts/v15/fixtures/quote-trade.journal.jsonl +compare_journal \ + v15-order-book \ + contracts/v15/fixtures/order-book.scenario.json \ + contracts/v15/fixtures/order-book.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index 1e73038..240086b 100644 --- a/scripts/check-documentation.py +++ b/scripts/check-documentation.py @@ -26,13 +26,13 @@ "docs/persistra.md", "SECURITY.md", "contracts/conformance/README.md", - "contracts/v14/README.md", + "contracts/v15/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v12/README.md", + "contracts/strategy/v13/README.md", "contracts/strategy/v3/README.md", "contracts/strategy/v2/README.md", "contracts/strategy/v1/README.md", diff --git a/scripts/release_artifacts.py b/scripts/release_artifacts.py index 62fd115..baa6858 100644 --- a/scripts/release_artifacts.py +++ b/scripts/release_artifacts.py @@ -376,8 +376,8 @@ def verify_release( ( "bin/trading-engine", "lib/trading_engine/opam", - "share/trading_engine/contracts/v14/scenario.schema.json", - "share/trading_engine/contracts/v14/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v15/scenario.schema.json", + "share/trading_engine/contracts/v15/fixtures/demo.scenario.json", "doc/trading_engine/README.md", ), epoch, @@ -388,7 +388,7 @@ def verify_release( ( "trading_engine.opam", "contracts/v1/scenario.schema.json", - "contracts/v14/fixtures/demo.scenario.json", + "contracts/v15/fixtures/demo.scenario.json", "docs/architecture.md", ".github/workflows/release-candidate.yml", ), @@ -400,8 +400,8 @@ def verify_release( ( "contracts/conformance/manifest.json", "contracts/v1/scenario.schema.json", - "contracts/v14/fixtures/demo.scenario.json", - "contracts/strategy/v12/message.schema.json", + "contracts/v15/fixtures/demo.scenario.json", + "contracts/strategy/v13/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v14/scenario.schema.json", + "contracts/v15/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 9291ade..dff75b2 100644 --- a/test/cli.t +++ b/test/cli.t @@ -2,7 +2,7 @@ 1.0.0 $ ../bin/main.exe --capabilities - {"engine_version":"1.0.0","scenario_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}}],"strategy_protocol_versions":["12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} + {"engine_version":"1.0.0","scenario_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1","completed_bar_next_open_v1","completed_bar_adverse_touch_v1","quote_trade_v1","order_book_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["15","14","13","12","11","10","9","8","7","6","5","4","3"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"2":["version","participation_bps","fee_schedules"],"1":["version","participation_bps","fixed_fee","fee_bps"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"fee_bps":{"minimum":0,"maximum":10000},"fixed_fee":{"minimum":"0","unit":"money"}}},{"name":"completed_bar_next_open_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"completed_bar_adverse_touch_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14","13"],"required_fields":["version","participation_bps","fee_schedules","spread_model","impact_model"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","spread_model","impact_model"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["completed_ohlcv_bars","bar_volume_for_linear_impact"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"half_spread_bps":{"minimum":0,"maximum":10000},"impact_coefficient_bps":{"minimum":0,"maximum":10000}}},{"name":"quote_trade_v1","configuration_versions":["1"],"scenario_contract_versions":["15","14"],"required_fields":["version","participation_bps","fee_schedules"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["causally_ordered_bid_ask_quotes","aggressor_classified_trades_for_passive_fills","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000}}},{"name":"order_book_v1","configuration_versions":["1"],"scenario_contract_versions":["15"],"required_fields":["version","participation_bps","fee_schedules","max_depth_levels"],"configuration_required_fields":{"1":["version","participation_bps","fee_schedules","max_depth_levels"]},"supported_order_types":["market","limit","stop","stop_limit"],"data_requirements":["slice_open_level_two_snapshot","contiguous_absolute_level_updates","aggressor_classified_depth_consuming_trades","completed_bars_for_valuation"],"limits":{"participation_bps":{"minimum":0,"maximum":10000},"max_depth_levels":{"minimum":1,"maximum":1024}}}],"strategy_protocol_versions":["13","12","11","10","9","8","7","6","5","4","3"],"resource_limits":{"version":"1","scenario_record_bytes":1048576,"strategy_message_bytes":1048576,"internal_events":100000,"catalog_instruments":4096,"intents_per_batch":4096,"artifact_record_bytes":2097152}} $ ../bin/main.exe --validate-only --input ../contracts/v8/fixtures/demo.scenario.json valid run=demo instruments=1 schedule=2 slices=4 scenario_sha256=85f7c99e0666159579c79256b3d0dc5f9c328e1275b79465fe1d4c93883e68f1 diff --git a/test/dune b/test/dune index 2c57ecb..1d45f1a 100644 --- a/test/dune +++ b/test/dune @@ -78,17 +78,20 @@ ../contracts/v13/journal.schema.json ../contracts/v13/scenario-stream.schema.json ../contracts/v13/scenario.schema.json - ../contracts/v14/fixtures/demo.journal.jsonl - ../contracts/v14/fixtures/demo.scenario.json - ../contracts/v14/fixtures/demo.scenario.jsonl - ../contracts/v14/fixtures/fill-clipped.journal.jsonl - ../contracts/v14/fixtures/fill-clipped.scenario.json - ../contracts/v14/fixtures/quote-trade.journal.jsonl - ../contracts/v14/fixtures/quote-trade.scenario.json - ../contracts/v14/fixtures/quote-trade.scenario.jsonl - ../contracts/v14/journal.schema.json - ../contracts/v14/scenario-stream.schema.json - ../contracts/v14/scenario.schema.json + ../contracts/v15/fixtures/demo.journal.jsonl + ../contracts/v15/fixtures/demo.scenario.json + ../contracts/v15/fixtures/demo.scenario.jsonl + ../contracts/v15/fixtures/fill-clipped.journal.jsonl + ../contracts/v15/fixtures/fill-clipped.scenario.json + ../contracts/v15/fixtures/quote-trade.journal.jsonl + ../contracts/v15/fixtures/quote-trade.scenario.json + ../contracts/v15/fixtures/quote-trade.scenario.jsonl + ../contracts/v15/fixtures/order-book.journal.jsonl + ../contracts/v15/fixtures/order-book.scenario.json + ../contracts/v15/fixtures/order-book.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -105,7 +108,7 @@ ../contracts/strategy/v9/fixtures/external.strategy.jsonl ../contracts/strategy/v10/fixtures/external.strategy.jsonl ../contracts/strategy/v11/fixtures/external.strategy.jsonl - ../contracts/strategy/v12/fixtures/external.strategy.jsonl + ../contracts/strategy/v13/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -128,64 +131,85 @@ (alias runtest) (deps validate_schemas.py - ../contracts/v14/fixtures/demo.journal.jsonl - ../contracts/v14/fixtures/demo.scenario.json - ../contracts/v14/fixtures/demo.scenario.jsonl - ../contracts/v14/journal.schema.json - ../contracts/v14/scenario-stream.schema.json - ../contracts/v14/scenario.schema.json) + ../contracts/v15/fixtures/demo.journal.jsonl + ../contracts/v15/fixtures/demo.scenario.json + ../contracts/v15/fixtures/demo.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v14/scenario.schema.json} - %{dep:../contracts/v14/scenario-stream.schema.json} - %{dep:../contracts/v14/journal.schema.json} - %{dep:../contracts/v14/fixtures/demo.scenario.json} - %{dep:../contracts/v14/fixtures/demo.scenario.jsonl} - %{dep:../contracts/v14/fixtures/demo.journal.jsonl}))) + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/scenario-stream.schema.json} + %{dep:../contracts/v15/journal.schema.json} + %{dep:../contracts/v15/fixtures/demo.scenario.json} + %{dep:../contracts/v15/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v15/fixtures/demo.journal.jsonl}))) (rule (alias runtest) (deps validate_schemas.py - ../contracts/v14/fixtures/quote-trade.journal.jsonl - ../contracts/v14/fixtures/quote-trade.scenario.json - ../contracts/v14/fixtures/quote-trade.scenario.jsonl - ../contracts/v14/journal.schema.json - ../contracts/v14/scenario-stream.schema.json - ../contracts/v14/scenario.schema.json) + ../contracts/v15/fixtures/quote-trade.journal.jsonl + ../contracts/v15/fixtures/quote-trade.scenario.json + ../contracts/v15/fixtures/quote-trade.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json) (action (run python3 %{dep:validate_schemas.py} - %{dep:../contracts/v14/scenario.schema.json} - %{dep:../contracts/v14/scenario-stream.schema.json} - %{dep:../contracts/v14/journal.schema.json} - %{dep:../contracts/v14/fixtures/quote-trade.scenario.json} - %{dep:../contracts/v14/fixtures/quote-trade.scenario.jsonl} - %{dep:../contracts/v14/fixtures/quote-trade.journal.jsonl}))) + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/scenario-stream.schema.json} + %{dep:../contracts/v15/journal.schema.json} + %{dep:../contracts/v15/fixtures/quote-trade.scenario.json} + %{dep:../contracts/v15/fixtures/quote-trade.scenario.jsonl} + %{dep:../contracts/v15/fixtures/quote-trade.journal.jsonl}))) (rule (alias runtest) (deps validate_strategy_schema.py - ../contracts/v14/scenario.schema.json - ../contracts/v14/journal.schema.json + ../contracts/v15/scenario.schema.json + ../contracts/v15/journal.schema.json ../contracts/diagnostic/v1/diagnostic.schema.json - ../contracts/strategy/v12/message.schema.json - ../contracts/strategy/v12/transcript.schema.json - ../contracts/strategy/v12/fixtures/external.strategy.jsonl) + ../contracts/strategy/v13/message.schema.json + ../contracts/strategy/v13/transcript.schema.json + ../contracts/strategy/v13/fixtures/external.strategy.jsonl) (action (run python3 %{dep:validate_strategy_schema.py} - %{dep:../contracts/v14/scenario.schema.json} - %{dep:../contracts/v14/journal.schema.json} + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/journal.schema.json} %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} - %{dep:../contracts/strategy/v12/message.schema.json} - %{dep:../contracts/strategy/v12/transcript.schema.json} - %{dep:../contracts/strategy/v12/fixtures/external.strategy.jsonl}))) + %{dep:../contracts/strategy/v13/message.schema.json} + %{dep:../contracts/strategy/v13/transcript.schema.json} + %{dep:../contracts/strategy/v13/fixtures/external.strategy.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v15/fixtures/order-book.journal.jsonl + ../contracts/v15/fixtures/order-book.scenario.json + ../contracts/v15/fixtures/order-book.scenario.jsonl + ../contracts/v15/journal.schema.json + ../contracts/v15/scenario-stream.schema.json + ../contracts/v15/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v15/scenario.schema.json} + %{dep:../contracts/v15/scenario-stream.schema.json} + %{dep:../contracts/v15/journal.schema.json} + %{dep:../contracts/v15/fixtures/order-book.scenario.json} + %{dep:../contracts/v15/fixtures/order-book.scenario.jsonl} + %{dep:../contracts/v15/fixtures/order-book.journal.jsonl}))) (rule (alias runtest) diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 6c487f9..a75dd33 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -113,6 +113,7 @@ let capabilities_describe_execution_contracts () = "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1"; "quote_trade_v1"; + "order_book_v1"; ] names; let model = List.hd models in @@ -136,7 +137,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "15"; "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] (strings "scenario_contract_versions"); Alcotest.(check (list string)) "required fields" diff --git a/test/test_domain.ml b/test/test_domain.ml index e3a50fd..8143222 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -181,8 +181,8 @@ let market_event_validation () = let base = market_slice 2L in Alcotest.(check string) "slice rendering includes market-event count" - "slice[2] bars=1 events=0 fx=1 actions=0 lifecycle=0 borrow=0 cash_rates=0 \ - failures=0" + "slice[2] bars=1 events=0 book_events=0 fx=1 actions=0 lifecycle=0 \ + borrow=0 cash_rates=0 failures=0" (Format.asprintf "%a" T.Market_slice.pp base); Alcotest.(check bool) "nonmonotonic ingest rejected" true @@ -198,6 +198,115 @@ let market_event_validation () = ~lifecycle_events:base.lifecycle_events ~market_events:[ first; second ])) +let order_book_event_validation () = + let instrument_id = instrument_id "book-validation" in + let event_at = timestamp "2026-01-03T14:30:00Z" in + let available_at = timestamp "2026-01-03T14:30:01Z" in + let received_at = timestamp "2026-01-03T14:30:02Z" in + let level price_value quantity_value = + T.Order_book_event.level ~price:(price price_value) + ~quantity:(quantity quantity_value) + |> ok + in + let snapshot bids asks = + T.Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:1L ~bids ~asks + in + Alcotest.(check bool) + "locked snapshot accepted" true + (Result.is_ok (snapshot [ level "100" "2" ] [ level "100" "3" ])); + Alcotest.(check bool) + "crossed snapshot rejected" true + (Result.is_error (snapshot [ level "101" "2" ] [ level "100" "3" ])); + Alcotest.(check bool) + "unordered duplicate depth rejected" true + (Result.is_error + (snapshot [ level "100" "2"; level "100" "3" ] [ level "101" "2" ])); + Alcotest.(check bool) + "empty side rejected" true + (Result.is_error (snapshot [] [ level "101" "2" ])); + Alcotest.(check bool) + "zero level rejected" true + (Result.is_error + (T.Order_book_event.level ~price:(price "100") + ~quantity:T.Scalar.Quantity.zero)); + Alcotest.(check bool) + "zero ingest sequence rejected" true + (Result.is_error + (T.Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:0L ~book_sequence:1L + ~bids:[ level "99" "1" ] + ~asks:[ level "101" "1" ])); + Alcotest.(check bool) + "zero book sequence rejected" true + (Result.is_error + (T.Order_book_event.snapshot ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:0L + ~bids:[ level "99" "1" ] + ~asks:[ level "101" "1" ])); + Alcotest.(check bool) + "availability before book event rejected" true + (Result.is_error + (T.Order_book_event.delete ~instrument_id ~event_at + ~available_at:(timestamp "2026-01-03T14:29:59Z") + ~received_at ~ingest_sequence:1L ~book_sequence:1L + ~side:T.Order_book_event.Bid ~price:(price "99"))); + Alcotest.(check bool) + "receipt before book availability rejected" true + (Result.is_error + (T.Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at:event_at ~ingest_sequence:1L ~book_sequence:1L + ~side:T.Order_book_event.Bid ~price:(price "99"))); + Alcotest.(check bool) + "zero set rejected" true + (Result.is_error + (T.Order_book_event.set ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:1L + ~side:T.Order_book_event.Bid ~price:(price "99") + ~quantity:T.Scalar.Quantity.zero)); + Alcotest.(check bool) + "zero book trade rejected" true + (Result.is_error + (T.Order_book_event.trade ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:1L ~book_sequence:1L ~price:(price "99") + ~quantity:T.Scalar.Quantity.zero ~aggressor_side:T.Market_event.Sell)); + Alcotest.(check bool) + "unknown book side rejected" true + (Result.is_error (T.Order_book_event.side_of_string "offer")); + let earlier = snapshot [ level "99" "1" ] [ level "101" "1" ] |> ok in + let later = + T.Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at:(timestamp "2026-01-03T14:30:03Z") + ~ingest_sequence:2L ~book_sequence:2L ~side:T.Order_book_event.Bid + ~price:(price "99") + |> ok + in + Alcotest.(check bool) + "book receipt orders replay" true + (T.Order_book_event.compare_replay_order earlier later < 0); + let same_time_later_sequence = + T.Order_book_event.delete ~instrument_id ~event_at ~available_at + ~received_at ~ingest_sequence:2L ~book_sequence:2L + ~side:T.Order_book_event.Bid ~price:(price "99") + |> ok + in + Alcotest.(check bool) + "book ingest sequence orders final replay tie" true + (T.Order_book_event.compare_replay_order earlier same_time_later_sequence + < 0); + List.iter + (fun (wire, side) -> + Alcotest.(check string) + (wire ^ " book side round trip") + wire + (T.Order_book_event.side_of_string wire + |> ok |> T.Order_book_event.side_to_string); + Alcotest.(check string) + (wire ^ " book side rendering") + wire + (T.Order_book_event.side_to_string side)) + [ ("bid", T.Order_book_event.Bid); ("ask", T.Order_book_event.Ask) ] + let bar_validation_boundaries () = let instrument_id = instrument_id "bar-validation" in let create ?(open_price = "100") ?(high_price = "110") ?(low_price = "90") @@ -397,6 +506,8 @@ let tests = portfolio_weight_rounds_toward_zero; Alcotest.test_case "market slice validation" `Quick market_slice_validation; Alcotest.test_case "market event validation" `Quick market_event_validation; + Alcotest.test_case "order-book event validation" `Quick + order_book_event_validation; Alcotest.test_case "bar validation boundaries" `Quick bar_validation_boundaries; Alcotest.test_case "corporate action validation boundaries" `Quick diff --git a/test/test_execution.ml b/test/test_execution.ml index 377b8ea..24856fb 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -77,6 +77,69 @@ let quote_trade_execution ?(participation_bps = 10_000) () = let fees = conservative_execution () |> T.Execution.fee_schedules in T.Execution.create_v2 ~participation_bps ~fee_schedules:fees |> ok +let book_level price_value quantity_value = + T.Order_book_event.level ~price:(price price_value) + ~quantity:(quantity quantity_value) + |> ok + +let book_snapshot ?(sequence = 1L) ?(second = 1) + ?(bids = [ book_level "99" "10" ]) ?(asks = [ book_level "101" "10" ]) () = + let event_at = market_event_time second in + T.Order_book_event.snapshot + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~bids ~asks + |> ok + +let book_set ?(sequence = 2L) ?(second = 2) ?(side = T.Order_book_event.Bid) + ?(price_value = "99") ?(quantity_value = "5") () = + let event_at = market_event_time second in + T.Order_book_event.set + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~side + ~price:(price price_value) ~quantity:(quantity quantity_value) + |> ok + +let book_delete ?(sequence = 2L) ?(second = 2) ?(side = T.Order_book_event.Bid) + ?(price_value = "99") () = + let event_at = market_event_time second in + T.Order_book_event.delete + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~side + ~price:(price price_value) + |> ok + +let book_trade ?(sequence = 2L) ?(second = 2) ?(price_value = "99") + ?(quantity_value = "5") ?(aggressor_side = T.Market_event.Sell) () = + let event_at = market_event_time second in + T.Order_book_event.trade + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~book_sequence:sequence ~price:(price price_value) + ~quantity:(quantity quantity_value) ~aggressor_side + |> ok + +let order_book_slice events = + let base = market_slice 2L in + T.Market_slice.create_v15 ~slice_sequence:base.slice_sequence + ~start_at:base.start_at ~end_at:base.end_at ~available_at:base.available_at + ~received_at:base.received_at ~bars:base.bars ~fx_rates:base.fx_rates + ~corporate_actions:base.corporate_actions + ~borrow_observations:base.borrow_observations + ~cash_rate_observations:base.cash_rate_observations + ~settlement_failures:base.settlement_failures + ~lifecycle_events:base.lifecycle_events ~market_events:[] + ~order_book_events:events + |> ok + +let order_book_execution ?(max_depth_levels = 10) () = + let fees = conservative_execution () |> T.Execution.fee_schedules in + T.Execution.create_order_book ~participation_bps:10_000 ~fee_schedules:fees + ~max_depth_levels + |> ok + let liquidity_name = function | T.Fee_schedule.Maker -> "maker" | Taker -> "taker" @@ -238,6 +301,337 @@ let quote_trade_stop_and_event_boundaries () = ~oms (quote_trade_slice [ old_event ]))) +let order_book_walks_depth_and_rejects_inconsistent_updates () = + let oms, order = oms_with_order (request ()) in + let snapshot = + book_snapshot ~asks:[ book_level "101" "4"; book_level "102" "6" ] () + in + let cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms + (order_book_slice [ snapshot ]) + |> ok + in + let first, advance = + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Proposed (proposal, advance) -> (proposal, advance) + | _ -> Alcotest.fail "best ask did not produce a fill" + in + Alcotest.check quantity_testable "first level quantity" (quantity "4") + first.quantity; + Alcotest.check price_testable "best ask first" (price "101") first.price; + Alcotest.(check bool) + "over-consumption rejected" true + (Result.is_error (advance (quantity "5"))); + Alcotest.(check bool) + "negative application rejected" true + (Result.is_error (advance (quantity "-1"))); + Alcotest.(check bool) + "off-lot application rejected" true + (Result.is_error (advance (quantity "0.5"))); + let cursor = advance first.quantity |> ok in + let applied = + fill ~quantity_value:"4" ~price_value:"101" ~executed_at:first.executed_at + order + in + let oms, _ = T.Oms.apply_fill oms applied |> ok in + (match T.Execution.next cursor ~oms |> ok with + | T.Execution.Proposed (second, _) -> + Alcotest.check quantity_testable "second level quantity" (quantity "6") + second.quantity; + Alcotest.check price_testable "second ask follows" (price "102") + second.price + | _ -> Alcotest.fail "second ask did not produce a fill"); + let fok_oms, _ = + oms_with_order + (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + in + let fok_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:fok_oms + (order_book_slice [ snapshot ]) + |> ok + in + (match T.Execution.next fok_cursor ~oms:fok_oms |> ok with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check quantity_testable "FOK sees total book depth" + (quantity "4") proposal.quantity + | _ -> Alcotest.fail "FOK ignored sufficient multi-level depth"); + let rejected events depth = + Result.is_error + (T.Execution.start_slice_order_book + (order_book_execution ~max_depth_levels:depth ()) + ~instruments:[ instrument () ] + ~oms (order_book_slice events)) + in + Alcotest.(check bool) + "sequence gap rejected" true + (rejected [ book_snapshot (); book_set ~sequence:3L () ] 10); + Alcotest.(check bool) + "missing delete rejected" true + (rejected [ book_snapshot (); book_delete ~price_value:"98" () ] 10); + Alcotest.(check bool) + "ask delete is applied" true + (Result.is_ok + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot (); + book_delete ~side:T.Order_book_event.Ask ~price_value:"101" (); + ]))); + Alcotest.(check bool) + "crossing update rejected" true + (rejected [ book_snapshot (); book_set ~price_value:"102" () ] 10); + Alcotest.(check bool) + "depth cap rejected" true + (rejected + [ book_snapshot ~bids:[ book_level "99" "1"; book_level "98" "1" ] () ] + 1); + Alcotest.(check bool) + "update before snapshot rejected" true + (rejected [ book_set () ] 10); + Alcotest.(check bool) + "duplicate snapshot rejected" true + (rejected [ book_snapshot (); book_snapshot ~sequence:2L ~second:2 () ] 10); + Alcotest.(check bool) + "trade beyond displayed depth rejected" true + (rejected + [ + book_snapshot ~bids:[ book_level "99" "2" ] (); + book_trade ~quantity_value:"3" (); + ] + 10); + let sell_oms, _ = oms_with_order (request ~side:T.Order.Sell ()) in + let sell_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:sell_oms + (order_book_slice + [ book_snapshot ~bids:[ book_level "99" "4"; book_level "98" "6" ] () ]) + |> ok + in + (match T.Execution.next sell_cursor ~oms:sell_oms |> ok with + | T.Execution.Proposed (proposal, advance) -> ( + Alcotest.check price_testable "sell walks best bid first" (price "99") + proposal.price; + let cursor = advance proposal.quantity |> ok in + let sell_order = T.Oms.find sell_oms proposal.order_id |> Option.get in + let applied = + fill ~quantity_value:"4" ~price_value:"99" + ~executed_at:proposal.executed_at sell_order + in + let sell_oms, _ = T.Oms.apply_fill sell_oms applied |> ok in + match T.Execution.next cursor ~oms:sell_oms |> ok with + | T.Execution.Proposed (next, _) -> + Alcotest.check price_testable "sell consumes next bid" (price "98") + next.price + | _ -> Alcotest.fail "second bid did not produce a sell fill") + | _ -> Alcotest.fail "best bid did not produce a sell fill"); + let added_oms, _ = + oms_with_order (request ~kind:(T.Order.Limit (price "102")) ()) + in + let added_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:added_oms + (order_book_slice + [ + book_snapshot ~asks:[ book_level "105" "10" ] (); + book_set ~side:T.Order_book_event.Ask ~price_value:"101" + ~quantity_value:"3" (); + ]) + |> ok + in + (match T.Execution.next added_cursor ~oms:added_oms |> ok with + | T.Execution.Proposed (proposal, advance) -> ( + Alcotest.check quantity_testable "added ask is bounded" (quantity "3") + proposal.quantity; + Alcotest.check price_testable "added ask becomes marketable" (price "101") + proposal.price; + let cursor = advance (quantity "2") |> ok in + let added_order = T.Oms.find added_oms proposal.order_id |> Option.get in + let applied = + fill ~quantity_value:"2" ~price_value:"101" + ~executed_at:proposal.executed_at added_order + in + let added_oms, _ = T.Oms.apply_fill added_oms applied |> ok in + match T.Execution.next cursor ~oms:added_oms |> ok with + | T.Execution.Proposed (remainder, _) -> + Alcotest.check quantity_testable "added ask remainder" (quantity "1") + remainder.quantity + | _ -> Alcotest.fail "added ask remainder did not execute") + | _ -> Alcotest.fail "added ask did not produce a fill"); + let passive_sell_oms, _ = + oms_with_order + (request ~side:T.Order.Sell ~kind:(T.Order.Limit (price "101")) ()) + in + let buy_trade_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:passive_sell_oms + (order_book_slice + [ + book_snapshot ~asks:[ book_level "101" "5" ] (); + book_trade ~aggressor_side:T.Market_event.Buy ~price_value:"101" + ~quantity_value:"2" (); + ]) + |> ok + in + (match T.Execution.next buy_trade_cursor ~oms:passive_sell_oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "buy trade should remain behind displayed ask queue"); + let unknown_trade_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot (); + book_trade ~aggressor_side:T.Market_event.Unknown + ~quantity_value:"100" (); + ]) + in + Alcotest.(check bool) + "unknown trade does not consume book" true + (Result.is_ok unknown_trade_cursor); + let triggered_stop side trigger = + let oms, order = + oms_with_order + (request_v8 ~side + ~kind:(T.Order.Stop (price trigger)) + ~time_in_force:T.Order.Gtc ()) + in + let cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms + (order_book_slice [ book_snapshot () ]) + |> ok + in + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Triggered (order_id, triggered_at, 2L, _) -> + Alcotest.(check string) + "order-book stop identity" + (T.Id.Order.to_string order.id) + (T.Id.Order.to_string order_id); + Alcotest.(check string) + "order-book stop event time" "2026-01-03T14:30:01.000000Z" + (T.Codec.ptime_to_string triggered_at) + | _ -> Alcotest.fail "order-book snapshot did not trigger stop" + in + triggered_stop T.Order.Buy "100"; + triggered_stop T.Order.Sell "100"; + let waiting_stop_oms, _ = + oms_with_order + (request_v8 + ~kind:(T.Order.Stop (price "200")) + ~time_in_force:T.Order.Gtc ()) + in + let waiting_stop_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:waiting_stop_oms + (order_book_slice [ book_snapshot () ]) + |> ok + in + (match T.Execution.next waiting_stop_cursor ~oms:waiting_stop_oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "untriggered order-book stop should remain dormant"); + let shallow_fok_oms, _ = + oms_with_order + (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + in + let shallow_fok_cursor = + T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:shallow_fok_oms + (order_book_slice [ book_snapshot ~asks:[ book_level "101" "3" ] () ]) + |> ok + in + (match T.Execution.next shallow_fok_cursor ~oms:shallow_fok_oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "FOK should reject insufficient order-book depth"); + Alcotest.(check bool) + "book model requires book configuration" true + (Result.is_error + (T.Execution.start_slice_order_book (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice [ book_snapshot () ]))); + Alcotest.(check bool) + "unknown book instrument rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument ~id:"other" () ] + ~oms:T.Oms.empty + (order_book_slice [ book_snapshot () ]))); + Alcotest.(check bool) + "snapshot coverage must be complete" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument (); instrument ~id:"other" () ] + ~oms:T.Oms.empty + (order_book_slice [ book_snapshot () ]))); + Alcotest.(check bool) + "off-tick book level rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument ~tick_size:"1" () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot + ~bids:[ book_level "99.5" "2" ] + ~asks:[ book_level "101" "2" ] + (); + ]))); + Alcotest.(check bool) + "off-lot book level rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument ~lot_size:"2" () ] + ~oms:T.Oms.empty + (order_book_slice + [ + book_snapshot + ~bids:[ book_level "99" "1" ] + ~asks:[ book_level "101" "2" ] + (); + ]))); + let old_at = timestamp "2026-01-02T14:30:00Z" in + let old_snapshot = + T.Order_book_event.snapshot + ~instrument_id:(instrument_id "test-equity") + ~event_at:old_at ~available_at:old_at ~received_at:old_at + ~ingest_sequence:1L ~book_sequence:1L + ~bids:[ book_level "99" "1" ] + ~asks:[ book_level "101" "1" ] + |> ok + in + Alcotest.(check bool) + "book event outside slice rejected" true + (Result.is_error + (T.Execution.start_slice_order_book (order_book_execution ()) + ~instruments:[ instrument () ] + ~oms:T.Oms.empty + (order_book_slice [ old_snapshot ]))); + Alcotest.(check bool) + "order-book depth must be positive" true + (Result.is_error + (T.Execution.create_order_book ~participation_bps:10_000 + ~fee_schedules:(T.Execution.fee_schedules (conservative_execution ())) + ~max_depth_levels:0)); + Alcotest.(check bool) + "order-book depth is capped" true + (Result.is_error + (T.Execution.create_order_book ~participation_bps:10_000 + ~fee_schedules:(T.Execution.fee_schedules (conservative_execution ())) + ~max_depth_levels:1025)) + let conservative_limit_models_diverge () = let engine = conservative_execution () in let limit = T.Order.Limit (price "100") in @@ -869,6 +1263,8 @@ let tests = quote_trade_limits_fok_and_continuations; Alcotest.test_case "quote replay stops and boundaries" `Quick quote_trade_stop_and_event_boundaries; + Alcotest.test_case "order book walks depth and rejects inconsistent updates" + `Quick order_book_walks_depth_and_rejects_inconsistent_updates; Alcotest.test_case "conservative limit models diverge" `Quick conservative_limit_models_diverge; Alcotest.test_case "conservative costs are attributed" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index 3c57b8f..bf17455 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,16 +2,21 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v14/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v15/fixtures/demo.scenario.json" In_channel.input_all let demo () = T.Scenario.of_string (demo_document ()) |> ok let demo_hash () = T.Sha256.digest_string (demo_document ()) -let stream_path = "../contracts/v14/fixtures/demo.scenario.jsonl" -let quote_trade_path = "../contracts/v14/fixtures/quote-trade.scenario.json" +let stream_path = "../contracts/v15/fixtures/demo.scenario.jsonl" +let quote_trade_path = "../contracts/v15/fixtures/quote-trade.scenario.json" let quote_trade_stream_path = - "../contracts/v14/fixtures/quote-trade.scenario.jsonl" + "../contracts/v15/fixtures/quote-trade.scenario.jsonl" + +let order_book_path = "../contracts/v15/fixtures/order-book.scenario.json" + +let order_book_stream_path = + "../contracts/v15/fixtures/order-book.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -79,7 +84,7 @@ let write_large_stream path slice_count = ~effective_at:start_at ~credit_rate_bps:0 ~debit_rate_bps:0 |> ok in - T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v15 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -93,13 +98,13 @@ let write_large_stream path slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate ] ~settlement_failures:[] - ~lifecycle_events:[] ~market_events:[] + ~lifecycle_events:[] ~market_events:[] ~order_book_events:[] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v14 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v15 market_slice); ("intents", `List []); ] in @@ -144,9 +149,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v14/scenario.schema.json"; - check_schema "../contracts/v14/scenario-stream.schema.json"; - check_schema "../contracts/v14/journal.schema.json" + check_schema "../contracts/v15/scenario.schema.json"; + check_schema "../contracts/v15/scenario-stream.schema.json"; + check_schema "../contracts/v15/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -207,7 +212,7 @@ let v12_distributions_and_lifecycle_parse () = |> ok in let market_slice = - T.Market_slice.create_v14 ~slice_sequence:1L + T.Market_slice.create_v15 ~slice_sequence:1L ~start_at:(timestamp "2026-01-02T14:30:00Z") ~end_at:(timestamp "2026-01-02T20:55:00Z") ~available_at:(timestamp "2026-01-02T21:00:00Z") @@ -249,7 +254,7 @@ let v12_distributions_and_lifecycle_parse () = reason = "acquisition"; }); ] - ~market_events:[] + ~market_events:[] ~order_book_events:[] |> ok in let document = @@ -346,7 +351,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v14 market_slice + (T.Codec.market_slice_to_yojson_v15 market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -436,8 +441,8 @@ let contract_version_is_required_and_supported () = let unsupported_diagnostic = T.Scenario.of_yojson unsupported |> error in Alcotest.(check string) "unsupported version diagnosed" - "unsupported scenario contract_version \"2\" (expected one of 14, 13, 12, \ - 11, 10, 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 15, 14, 13, \ + 12, 11, 10, 9, 8, 7, 6, 5, 4, 3)" (T.Diagnostic.to_human unsupported_diagnostic); Alcotest.(check string) "unsupported version code" "scenario.unsupported_contract" @@ -587,7 +592,7 @@ let dense_schedule_document slice_count = ~effective_at:start_at ~credit_rate_bps:100 ~debit_rate_bps:200 |> ok in - T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v15 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (time_offset + 1)) ~available_at:(add_seconds base (time_offset + 2)) ~received_at:(add_seconds base (time_offset + 3)) @@ -601,7 +606,8 @@ let dense_schedule_document slice_count = ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v14) + ~order_book_events:[] + |> ok |> T.Codec.market_slice_to_yojson_v15) in let schedule = List.init slice_count (fun offset -> @@ -1185,7 +1191,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v14/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v15/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -1213,7 +1219,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v14/fixtures/fill-clipped.scenario.json" + "../contracts/v15/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -1227,7 +1233,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v14/fixtures/fill-clipped.journal.jsonl" + "../contracts/v15/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1247,7 +1253,7 @@ let quote_trade_replay_is_causal_and_stream_equivalent () = in let golden = In_channel.with_open_bin - "../contracts/v14/fixtures/quote-trade.journal.jsonl" In_channel.input_all + "../contracts/v15/fixtures/quote-trade.journal.jsonl" In_channel.input_all in Alcotest.(check string) "quote/trade golden journal" golden batch_journal; let fills = @@ -1290,6 +1296,64 @@ let quote_trade_replay_is_causal_and_stream_equivalent () = "quote/trade stream and batch journals agree" expected (In_channel.with_open_bin journal In_channel.input_all)) +let order_book_replay_is_bounded_and_stream_equivalent () = + let document = + In_channel.with_open_bin order_book_path In_channel.input_all + in + let scenario = T.Scenario.of_string document |> ok in + let batch = + T.Replay.run ~scenario_sha256:(T.Sha256.digest_string document) scenario + |> ok + in + let actual = + batch.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let golden = + In_channel.with_open_bin + "../contracts/v15/fixtures/order-book.journal.jsonl" In_channel.input_all + in + Alcotest.(check string) "order-book golden journal" golden actual; + let fills = + List.filter_map + (fun (audit : T.Audit.t) -> + match audit.event with + | T.Audit.Fill_applied fill -> Some fill + | _ -> None) + batch.audits + in + Alcotest.(check (list string)) + "queue reduction precedes deterministic partial maker fills" + [ "4@100@2026-02-03T14:35:00.000000Z"; "6@100@2026-02-03T14:36:00.000000Z" ] + (List.map + (fun (fill : T.Fill.t) -> + Printf.sprintf "%s@%s@%s" + (T.Scalar.Quantity.to_decimal_string fill.quantity) + (T.Scalar.Price.to_decimal_string fill.price) + (T.Codec.ptime_to_string fill.executed_at)) + fills); + let stream_hash = T.Sha256.digest_file order_book_stream_path |> ok in + let expected = + T.Replay.run ~scenario_sha256:stream_hash scenario |> ok |> fun result -> + result.audits |> List.map T.Codec.audit_to_string |> String.concat "\n" + |> fun value -> value ^ "\n" + in + let journal = Filename.temp_file "trading-engine-order-book" ".jsonl" in + Sys.remove journal; + Fun.protect + ~finally:(fun () -> + if Sys.file_exists journal then Sys.remove journal; + if Sys.file_exists (journal ^ ".partial") then + Sys.remove (journal ^ ".partial")) + (fun () -> + let streamed = + T.Replay.run_stream ~journal_path:journal order_book_stream_path |> ok + in + Alcotest.(check int64) "two streamed slices" 2L streamed.slice_count; + Alcotest.(check string) + "order-book stream and batch journals agree" expected + (In_channel.with_open_bin journal In_channel.input_all)) + let journal_is_created_exclusively () = let scenario = demo () in let existing = Filename.temp_file "trading-engine" ".jsonl" in @@ -1634,6 +1698,8 @@ let tests = fill_clipping_fixture_reconciles; Alcotest.test_case "quote/trade replay is causal and stream equivalent" `Quick quote_trade_replay_is_causal_and_stream_equivalent; + Alcotest.test_case "order-book replay is bounded and stream equivalent" + `Quick order_book_replay_is_bounded_and_stream_equivalent; Alcotest.test_case "exclusive journal creation" `Quick journal_is_created_exclusively; Alcotest.test_case "exclusive journal finalization" `Quick diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 9ad4f27..f63b960 100644 --- a/test/test_strategy_protocol.ml +++ b/test/test_strategy_protocol.ml @@ -45,7 +45,7 @@ let initialize_message_is_complete () = T.Strategy_protocol.initialize_message ~sequence:1L (initialization ()) in Alcotest.(check string) - "protocol version" "12" + "protocol version" "13" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -265,7 +265,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "12"); + ("strategy_protocol_version", `String "13"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload);