diff --git a/CHANGELOG.md b/CHANGELOG.md index 24a4c9e..b7aa900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- 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 + and protocol v11 as frozen compatibility contracts. - Add conservative next-open and adverse-touch completed-bar execution models with strict fixed spread and linear participation-impact configuration, explicit missing-volume policy, tick-aligned prices, and separate price-component audit attribution. diff --git a/README.md b/README.md index 8b92f9e..f307b7d 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,12 @@ 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 next-open and adverse-touch models while freezing - `completed_bar_v1` + capability descriptors; v13 adds conservative bar models and v14 adds causal quote/trade 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 - 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 @@ -94,7 +96,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v13/fixtures/demo.scenario.json \ + --input contracts/v14/fixtures/demo.scenario.json \ --validate-only ``` @@ -102,7 +104,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v13/fixtures/demo.scenario.json \ + --input contracts/v14/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -110,7 +112,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v13/fixtures/demo.scenario.jsonl \ + --input contracts/v14/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -119,7 +121,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v11/fixtures/external.scenario.json \ + --input contracts/strategy/v12/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -234,19 +236,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 v13 and conformance fixtures](contracts/v13/README.md) +- [Current contract v14 and conformance fixtures](contracts/v14/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v13/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v13/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v13/journal.schema.json) -- [External strategy protocol v11](contracts/strategy/v11/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) - [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/v11/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v11/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v12/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v12/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 a32b12a..3b62267 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -847,7 +847,9 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 2, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "1", "protocol_version": "9", "mutations": [], @@ -861,7 +863,9 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 4, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "2", "protocol_version": "9", "mutations": [], @@ -875,7 +879,9 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "protocol_version": "9", "mutations": [], @@ -889,12 +895,28 @@ "kind": "strategy_response", "source": "strategy/v9/fixtures/external.strategy.jsonl", "record": 14, - "extract": ["message"], + "extract": [ + "message" + ], "expected_sequence": "7", "protocol_version": "9", "mutations": [ - { "op": "replace", "path": ["message_type"], "value": "error" }, - { "op": "replace", "path": ["payload"], "value": { "message": "fixture failure" } } + { + "op": "replace", + "path": [ + "message_type" + ], + "value": "error" + }, + { + "op": "replace", + "path": [ + "payload" + ], + "value": { + "message": "fixture failure" + } + } ], "schema_expectation": "accept", "runtime_expectation": "accept", @@ -1177,7 +1199,10 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": { "json_path": "$", "sequence": "1" }, + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { @@ -1216,7 +1241,10 @@ "strategy_protocol_version": "10", "strategy_sequence": "1", "message_type": "ready", - "payload": { "strategy_name": "conformance", "strategy_version": null } + "payload": { + "strategy_name": "conformance", + "strategy_version": null + } }, "mutations": [], "schema_expectation": "accept", @@ -1230,7 +1258,9 @@ "strategy_protocol_version": "10", "strategy_sequence": "2", "message_type": "intents", - "payload": { "intents": [] } + "payload": { + "intents": [] + } }, "mutations": [], "schema_expectation": "accept", @@ -1244,7 +1274,9 @@ "strategy_protocol_version": "10", "strategy_sequence": "7", "message_type": "error", - "payload": { "message": "fixture failure" } + "payload": { + "message": "fixture failure" + } }, "mutations": [], "schema_expectation": "accept" @@ -1262,7 +1294,10 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": { "json_path": "$", "sequence": "1" }, + "context": { + "json_path": "$", + "sequence": "1" + }, "cause": null }, "evidence": { @@ -1300,7 +1335,10 @@ "strategy_protocol_version": "11", "strategy_sequence": "1", "message_type": "ready", - "payload": { "strategy_name": "conformance", "strategy_version": null } + "payload": { + "strategy_name": "conformance", + "strategy_version": null + } }, "mutations": [], "schema_expectation": "accept", @@ -1314,7 +1352,9 @@ "strategy_protocol_version": "11", "strategy_sequence": "2", "message_type": "intents", - "payload": { "intents": [] } + "payload": { + "intents": [] + } }, "mutations": [], "schema_expectation": "accept", @@ -1328,7 +1368,9 @@ "strategy_protocol_version": "11", "strategy_sequence": "7", "message_type": "error", - "payload": { "message": "fixture failure" } + "payload": { + "message": "fixture failure" + } }, "mutations": [], "schema_expectation": "accept" @@ -1346,7 +1388,113 @@ "code": "strategy.protocol", "phase": "strategy", "message": "strategy initialization: invalid strategy response JSON", - "context": { "json_path": "$", "sequence": "1" }, + "context": { + "json_path": "$", + "sequence": "1" + }, + "cause": null + }, + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": false + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "scenario-v14-valid", + "artifact": "scenario-v14", + "kind": "scenario", + "source": "v14/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "scenario-v14-quote-trade-valid", + "artifact": "scenario-v14", + "kind": "scenario", + "source": "v14/fixtures/quote-trade.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "scenario-stream-v14-valid", + "artifact": "scenario-stream-v14", + "kind": "scenario_stream", + "source": "v14/fixtures/quote-trade.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "strategy-ready-valid-v12", + "artifact": "strategy-message-v12", + "instance": { + "strategy_protocol_version": "12", + "strategy_sequence": "1", + "message_type": "ready", + "payload": { + "strategy_name": "conformance", + "strategy_version": null + } + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "ready" + }, + { + "name": "strategy-intents-valid-v12", + "artifact": "strategy-message-v12", + "instance": { + "strategy_protocol_version": "12", + "strategy_sequence": "2", + "message_type": "intents", + "payload": { + "intents": [] + } + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "intents" + }, + { + "name": "strategy-error-valid-v12", + "artifact": "strategy-message-v12", + "instance": { + "strategy_protocol_version": "12", + "strategy_sequence": "7", + "message_type": "error", + "payload": { + "message": "fixture failure" + } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v12-rejected-response-branch", + "artifact": "strategy-transcript-v12", + "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": { diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index 97576ee..6d5e637 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -715,9 +715,18 @@ "version_field": "contract_version", "version": "11", "sources": [ - { "path": "v11/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v11/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v9/fixtures/external.scenario.json", "format": "json" } + { + "path": "v11/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v11/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v9/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -726,8 +735,14 @@ "version_field": "contract_version", "version": "11", "sources": [ - { "path": "v11/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v9/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v11/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v9/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -736,8 +751,14 @@ "version_field": "contract_version", "version": "11", "sources": [ - { "path": "v11/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v11/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v11/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v11/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -746,7 +767,13 @@ "version_field": "strategy_protocol_version", "version": "9", "sources": [ - { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + { + "path": "strategy/v9/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -755,7 +782,10 @@ "version_field": "strategy_protocol_version", "version": "9", "sources": [ - { "path": "strategy/v9/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v9/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -764,9 +794,18 @@ "version_field": "contract_version", "version": "12", "sources": [ - { "path": "v12/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v12/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v10/fixtures/external.scenario.json", "format": "json" } + { + "path": "v12/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v12/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v10/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -775,8 +814,14 @@ "version_field": "contract_version", "version": "12", "sources": [ - { "path": "v12/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v10/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v12/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v10/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -785,8 +830,14 @@ "version_field": "contract_version", "version": "12", "sources": [ - { "path": "v12/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v12/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v12/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v12/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -795,7 +846,13 @@ "version_field": "strategy_protocol_version", "version": "10", "sources": [ - { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + { + "path": "strategy/v10/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -804,7 +861,10 @@ "version_field": "strategy_protocol_version", "version": "10", "sources": [ - { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v10/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] }, { @@ -813,9 +873,18 @@ "version_field": "contract_version", "version": "13", "sources": [ - { "path": "v13/fixtures/demo.scenario.json", "format": "json" }, - { "path": "v13/fixtures/fill-clipped.scenario.json", "format": "json" }, - { "path": "strategy/v11/fixtures/external.scenario.json", "format": "json" } + { + "path": "v13/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v13/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "strategy/v11/fixtures/external.scenario.json", + "format": "json" + } ] }, { @@ -824,8 +893,14 @@ "version_field": "contract_version", "version": "13", "sources": [ - { "path": "v13/fixtures/demo.scenario.jsonl", "format": "jsonl" }, - { "path": "strategy/v11/fixtures/external.scenario.jsonl", "format": "jsonl" } + { + "path": "v13/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v11/fixtures/external.scenario.jsonl", + "format": "jsonl" + } ] }, { @@ -834,8 +909,14 @@ "version_field": "contract_version", "version": "13", "sources": [ - { "path": "v13/fixtures/demo.journal.jsonl", "format": "jsonl" }, - { "path": "v13/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + { + "path": "v13/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v13/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + } ] }, { @@ -844,7 +925,13 @@ "version_field": "strategy_protocol_version", "version": "11", "sources": [ - { "path": "strategy/v11/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + { + "path": "strategy/v11/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } ] }, { @@ -853,7 +940,101 @@ "version_field": "strategy_protocol_version", "version": "11", "sources": [ - { "path": "strategy/v11/fixtures/external.strategy.jsonl", "format": "jsonl" } + { + "path": "strategy/v11/fixtures/external.strategy.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "scenario-v14", + "schema": "v14/scenario.schema.json", + "version_field": "contract_version", + "version": "14", + "sources": [ + { + "path": "v14/fixtures/demo.scenario.json", + "format": "json" + }, + { + "path": "v14/fixtures/fill-clipped.scenario.json", + "format": "json" + }, + { + "path": "v14/fixtures/quote-trade.scenario.json", + "format": "json" + }, + { + "path": "strategy/v12/fixtures/external.scenario.json", + "format": "json" + } + ] + }, + { + "name": "scenario-stream-v14", + "schema": "v14/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "14", + "sources": [ + { + "path": "v14/fixtures/demo.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "v14/fixtures/quote-trade.scenario.jsonl", + "format": "jsonl" + }, + { + "path": "strategy/v12/fixtures/external.scenario.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "journal-v14", + "schema": "v14/journal.schema.json", + "version_field": "contract_version", + "version": "14", + "sources": [ + { + "path": "v14/fixtures/demo.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v14/fixtures/fill-clipped.journal.jsonl", + "format": "jsonl" + }, + { + "path": "v14/fixtures/quote-trade.journal.jsonl", + "format": "jsonl" + } + ] + }, + { + "name": "strategy-message-v12", + "schema": "strategy/v12/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "12", + "sources": [ + { + "path": "strategy/v12/fixtures/external.strategy.jsonl", + "format": "jsonl", + "extract": [ + "message" + ] + } + ] + }, + { + "name": "strategy-transcript-v12", + "schema": "strategy/v12/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "12", + "sources": [ + { + "path": "strategy/v12/fixtures/external.strategy.jsonl", + "format": "jsonl" + } ] } ] diff --git a/contracts/strategy/v12/README.md b/contracts/strategy/v12/README.md new file mode 100644 index 0000000..5aa17ef --- /dev/null +++ b/contracts/strategy/v12/README.md @@ -0,0 +1,59 @@ +# External strategy protocol v12 + +Version 12 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: "12"` 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 v14 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 v14 intent shapes. Market-slice events include lifecycle transitions and +the expanded corporate-action catalog, plus causally ordered quote/trade market events. + +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/v12/dune b/contracts/strategy/v12/dune new file mode 100644 index 0000000..3351a8f --- /dev/null +++ b/contracts/strategy/v12/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v12/message.schema.json) + (transcript.schema.json as contracts/strategy/v12/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v12/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v12/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v12/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v12/fixtures/external.scenario.json b/contracts/strategy/v12/fixtures/external.scenario.json new file mode 100644 index 0000000..724125a --- /dev/null +++ b/contracts/strategy/v12/fixtures/external.scenario.json @@ -0,0 +1,306 @@ +{ + "contract_version": "14", + "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": [] + }, + { + "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": [] + } + ], + "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/v12/fixtures/external.scenario.jsonl b/contracts/strategy/v12/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..793db28 --- /dev/null +++ b/contracts/strategy/v12/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"14","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":"14","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"14","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":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"14","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v12/fixtures/external.strategy.jsonl b/contracts/strategy/v12/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..acbe035 --- /dev/null +++ b/contracts/strategy/v12/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"12","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"14","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":"12","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"12","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","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":[]}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","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":"12","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","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":"12","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","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":"12","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","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":"12","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","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":[]}}}}} +{"strategy_protocol_version":"12","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"12","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"12","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"12","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"12","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v12/message.schema.json b/contracts/strategy/v12/message.schema.json new file mode 100644 index 0000000..58c9e0c --- /dev/null +++ b/contracts/strategy/v12/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/v12/message.schema.json", + "title": "Trading Engine external strategy protocol v12 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": "12" }, + "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": "14" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/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/v14/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/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/v14/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/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/v14/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/v14/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/v14/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/v12/transcript.schema.json b/contracts/strategy/v12/transcript.schema.json new file mode 100644 index 0000000..f00912f --- /dev/null +++ b/contracts/strategy/v12/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/v12/transcript.schema.json", + "title": "Trading Engine external strategy protocol v12 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": "12" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v12/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/v14/README.md b/contracts/v14/README.md new file mode 100644 index 0000000..cf247f4 --- /dev/null +++ b/contracts/v14/README.md @@ -0,0 +1,102 @@ +# Trading Engine contract v14 + +This directory is the authoritative v14 process and file contract shared by Trading Engine and its +clients. Versions 13 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 v14 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 v14 scenario, stream record, and journal record carries `"contract_version": "14"`. + +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. diff --git a/contracts/v14/dune b/contracts/v14/dune new file mode 100644 index 0000000..17e522a --- /dev/null +++ b/contracts/v14/dune @@ -0,0 +1,27 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v14/journal.schema.json) + (scenario-stream.schema.json as contracts/v14/scenario-stream.schema.json) + (scenario.schema.json as contracts/v14/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v14/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v14/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v14/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v14/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v14/fixtures/fill-clipped.scenario.json) + (fixtures/quote-trade.journal.jsonl + as + contracts/v14/fixtures/quote-trade.journal.jsonl) + (fixtures/quote-trade.scenario.json + as + contracts/v14/fixtures/quote-trade.scenario.json) + (fixtures/quote-trade.scenario.jsonl + as + contracts/v14/fixtures/quote-trade.scenario.jsonl))) diff --git a/contracts/v14/fixtures/demo.journal.jsonl b/contracts/v14/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..3f41591 --- /dev/null +++ b/contracts/v14/fixtures/demo.journal.jsonl @@ -0,0 +1,29 @@ +{"contract_version":"14","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":"49cac47b2abf617f1610d65a29984ab6cd848cd42c28b5d190fb34369365b18f","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":"14","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":"49cac47b2abf617f1610d65a29984ab6cd848cd42c28b5d190fb34369365b18f","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/v14/fixtures/demo.scenario.json b/contracts/v14/fixtures/demo.scenario.json new file mode 100644 index 0000000..55e7f7b --- /dev/null +++ b/contracts/v14/fixtures/demo.scenario.json @@ -0,0 +1,461 @@ +{ + "contract_version": "14", + "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": [] + }, + { + "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": [] + }, + { + "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": [] + }, + { + "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": [] + } + ], + "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/v14/fixtures/demo.scenario.jsonl b/contracts/v14/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..ae6e155 --- /dev/null +++ b/contracts/v14/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"14","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":"14","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"14","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":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"14","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":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"14","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":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"14","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v14/fixtures/fill-clipped.journal.jsonl b/contracts/v14/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..1a1c149 --- /dev/null +++ b/contracts/v14/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"14","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":"77457107873a439fdb669f03d65eaf3a7843e1649a4a0036e68a958c7a48baad","execution_model":"completed_bar_v1"}} +{"contract_version":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":"14","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":"77457107873a439fdb669f03d65eaf3a7843e1649a4a0036e68a958c7a48baad","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/v14/fixtures/fill-clipped.scenario.json b/contracts/v14/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..afee413 --- /dev/null +++ b/contracts/v14/fixtures/fill-clipped.scenario.json @@ -0,0 +1,271 @@ +{ + "contract_version": "14", + "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": [] + }, + { + "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": [] + } + ], + "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/v14/fixtures/quote-trade.journal.jsonl b/contracts/v14/fixtures/quote-trade.journal.jsonl new file mode 100644 index 0000000..0f729da --- /dev/null +++ b/contracts/v14/fixtures/quote-trade.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"14","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":"fc4ba762e5464546c4ec601df566c99e6c5995928a18e5b72434775ed20b62ac","execution_model":"quote_trade_v1"}} +{"contract_version":"14","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":"14","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":"14","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":[]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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"}]}} +{"contract_version":"14","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":"14","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":"14","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":"14","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":"14","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":"fc4ba762e5464546c4ec601df566c99e6c5995928a18e5b72434775ed20b62ac","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/v14/fixtures/quote-trade.scenario.json b/contracts/v14/fixtures/quote-trade.scenario.json new file mode 100644 index 0000000..230f846 --- /dev/null +++ b/contracts/v14/fixtures/quote-trade.scenario.json @@ -0,0 +1,317 @@ +{ + "contract_version": "14", + "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": [] + }, + { + "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" + } + ] + } + ], + "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/v14/fixtures/quote-trade.scenario.jsonl b/contracts/v14/fixtures/quote-trade.scenario.jsonl new file mode 100644 index 0000000..b399827 --- /dev/null +++ b/contracts/v14/fixtures/quote-trade.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"14","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":"14","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":[]},"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":"14","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"}]},"intents":[]}} +{"contract_version":"14","scenario_sequence":"4","record_type":"scenario_end","payload":{"slice_count":"2"}} diff --git a/contracts/v14/journal.schema.json b/contracts/v14/journal.schema.json new file mode 100644 index 0000000..399e678 --- /dev/null +++ b/contracts/v14/journal.schema.json @@ -0,0 +1,2420 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v14/journal.schema.json", + "title": "Trading Engine v14 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": "14" + }, + "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"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/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" + ], + "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/v14/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/lifecycleEvent" + } + }, + "market_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/marketEvent" + } + } + } + }, + "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/v14/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/v14/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/v14/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/v14/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"] + }, + "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/v14/scenario-stream.schema.json b/contracts/v14/scenario-stream.schema.json new file mode 100644 index 0000000..6093644 --- /dev/null +++ b/contracts/v14/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/v14/scenario-stream.schema.json", + "title": "Trading Engine v14 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": "14" }, + "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": "14" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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": "14" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/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/v14/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v14/scenario.schema.json b/contracts/v14/scenario.schema.json new file mode 100644 index 0000000..f4117b5 --- /dev/null +++ b/contracts/v14/scenario.schema.json @@ -0,0 +1,770 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v14/scenario.schema.json", + "title": "Trading Engine v14 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": "14" }, + "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" } + } + } + ] + }, + "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" } } + } + }, + "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", "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" } }, + "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"] } + } + } + ] + }, + "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 fdfddd4..52529d2 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/v13/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v14/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 4c10789..ef3971a 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,8 +21,8 @@ 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 v13 batch, stream, journal, and -strategy-v8 fixtures, including financing attribution and the reconciled first valuation. +The full test suite additionally validates and replays the current v14 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 once against its full pinned commit; it is not repeated across dependency or operating-system diff --git a/docs/execution-model.md b/docs/execution-model.md index 6a9e18f..597573e 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 v13 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, and -`completed_bar_adverse_touch_v1`; embedders can inject another module through +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 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 v13 envelope separates selection from +Each compiled model owns a strict configuration contract. The v14 envelope separates selection from model-specific parameters: ```json @@ -51,6 +51,14 @@ The conservative models use strict configuration version `"1"`. Both require `sp `model: "linear_participation_v1"`, `coefficient_bps`, and `missing_volume_policy`. The latter is either `reject` or `zero_impact`; no ambient spread or volume data is inferred. +The quote/trade model also uses configuration version `"1"`, with `participation_bps` and the same +fee-schedule catalog. It consumes each slice's events in `(available_at, received_at, +ingest_sequence)` order. Market orders and marketable limits consume only the displayed quote size +on their side. Passive buys consume only sell-aggressor trades at or below their limit; passive +sells consume only buy-aggressor trades at or above it. An `unknown` aggressor never supplies a +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. + ## 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 7a23c24..68864ad 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 v13 while retaining v12 through v3 and +structural checks. The engine advertises current contract v14 while retaining v13 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 v11](../contracts/strategy/v11/README.md). Persistra's host turns protocol +[strategy protocol v12](../contracts/strategy/v12/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 v13 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v14 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 v11; a host must complete its exact initialization, event, + external boundary is strategy v12; 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 8111c8f..fe9e0a5 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 v13 demo](../contracts/v13/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v13/scenario.schema.json) provides structural validation. +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. 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/v13/scenario-stream.schema.json) validates each line, -and [the v13 stream fixture](../contracts/v13/fixtures/demo.scenario.jsonl) is the canonical example. +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 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; v13 is `"13"` | +| `contract_version` | Required string identifying this file contract; v14 is `"14"` | | `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 | @@ -130,7 +130,7 @@ order types, data requirements, and limits through `--capabilities.execution_mod v8 and earlier contracts retain completed-bar configuration version `"1"`; v3 and v4 preserve their flat execution object unchanged. -Contract v13 also accepts `completed_bar_next_open_v1` and +Contract v13 introduced `completed_bar_next_open_v1` and `completed_bar_adverse_touch_v1`, each with strict configuration version `"1"`. They retain `participation_bps` and `fee_schedules`, and additionally require: @@ -234,7 +234,7 @@ has zero available quantity. The latest observation remains active until replace missing-data handling, `reject_order` or `clip_fill` locate behavior, and `reject_new_shorts` or `close_out` recall behavior. -The v13 `settlement` object selects `total_cash` or `settled_cash` buying power and +The v12 `settlement` object selects `total_cash` or `settled_cash` buying power and `total_positions` or `settled_positions` availability. Its immutable calendars contain ordered canonical business dates, and each instrument has exactly one calendar and a lag from zero through 30 business days. A fill updates economic accounting immediately and creates a deterministic @@ -262,12 +262,21 @@ terminal and require either `hold` or an explicit quote-currency `cash_out` pric terminal events cancel active orders. Terminal events set persistent target exposure to zero and cash-out clears the position with exact realized-P&L attribution. +Version 14 slices add `market_events`. A quote records bid/ask prices and displayed quantities; a +trade records price, quantity, and `buy`, `sell`, or `unknown` aggressor side. Every event also +records `event_at`, `available_at`, `received_at`, and a positive `ingest_sequence`. Events are +strictly ordered by availability, receipt, and ingest sequence; economic time cannot follow +availability, and no event may escape its containing slice's time or observability boundary. +Prices and quantities align to the instrument tick and lot. The +[`quote-trade` fixture](../contracts/v14/fixtures/quote-trade.scenario.json) demonstrates passive +fills and has an equivalent bounded JSON Lines replay. + 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/v13/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v14/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 98d2c67..2ec80da 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -359,6 +359,36 @@ let settlement_failure_to_yojson failure = ("reason", string failure.reason); ] +let market_event_to_yojson event = + let common = + [ + ("instrument_id", instrument_id event.Market_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); + ] + in + match event.kind with + | Market_event.Quote { bid_price; bid_quantity; ask_price; ask_quantity } -> + `Assoc + ((("type", string "quote") :: common) + @ [ + ("bid_price", price bid_price); + ("bid_quantity", quantity bid_quantity); + ("ask_price", price ask_price); + ("ask_quantity", quantity ask_quantity); + ]) + | Market_event.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 [ @@ -375,9 +405,10 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ); ] |> function - | `Assoc fields when List.mem contract_version [ "13"; "12"; "11"; "10" ] -> + | `Assoc fields + when List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] -> let settlement = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then [ ( "settlement_failures", `List @@ -387,7 +418,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let lifecycle = - if List.mem contract_version [ "13"; "12" ] then + if List.mem contract_version [ "14"; "13"; "12" ] then [ ( "lifecycle_events", `List @@ -396,6 +427,16 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ] else [] in + let market_events = + if String.equal contract_version "14" then + [ + ( "market_events", + `List + (List.map market_event_to_yojson + market_slice.Market_slice.market_events) ); + ] + else [] + in `Assoc (fields @ [ @@ -408,7 +449,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) + @ settlement @ lifecycle @ market_events) | json -> json let market_slice_to_yojson market_slice = @@ -426,6 +467,9 @@ let market_slice_to_yojson_v12 market_slice = let market_slice_to_yojson_v13 market_slice = versioned_market_slice_to_yojson ~contract_version:"13" market_slice +let market_slice_to_yojson_v14 market_slice = + versioned_market_slice_to_yojson ~contract_version:"14" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -529,7 +573,7 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order @@ -727,7 +771,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if List.mem contract_version [ "13"; "12"; "11" ] then + (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 @@ -736,7 +780,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if List.mem contract_version [ "13"; "12"; "11" ] then + (if List.mem contract_version [ "14"; "13"; "12"; "11" ] then position_attribution_to_yojson_v11 else if String.equal contract_version "9" @@ -746,15 +790,15 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = valuation.positions) ); ] |> function - | `Assoc fields when List.mem contract_version [ "13"; "12"; "11"; "10"; "9" ] - -> + | `Assoc fields + when List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] -> let financing = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -801,7 +845,8 @@ 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 [ "13"; "12"; "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] + then fields @ [ ( "group_exposures", @@ -942,7 +987,7 @@ let payload_to_yojson ~contract_version = function ("final_price", price attribution.final_price); ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9" ] then fill_to_yojson_v9 fill else fill_to_yojson fill | Audit.Settlement_instruction_created instruction diff --git a/lib/codec.mli b/lib/codec.mli index 0b10ce3..a097970 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -8,6 +8,7 @@ val market_slice_to_yojson_v10 : Market_slice.t -> Yojson.Safe.t 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 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 6f14005..da5fc9b 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "13" -let previous_version = "12" +let version = "14" +let previous_version = "13" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "12"; "11"; "10"; "9"; @@ -18,8 +19,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "11" -let previous_strategy_protocol_version = "10" +let strategy_protocol_version = "12" +let previous_strategy_protocol_version = "11" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -38,6 +39,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "10"; "9"; "8"; "7"; diff --git a/lib/engine.ml b/lib/engine.ml index dfe9c08..c5443b0 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -69,6 +69,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 valid_sha256 value = String.length value = 64 diff --git a/lib/engine.mli b/lib/engine.mli index 2cd959b..4849421 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -62,6 +62,17 @@ val config_v13 : max_internal_events:int -> (config, string) result +val config_v14 : + 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 7a4d04c..1a4fca0 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -538,6 +538,278 @@ let start_slice_next_open state = start_slice_with_policy Next_open_only state let start_slice_adverse_touch state = start_slice_with_policy Adverse_touch state +type observable_liquidity = + | Quote_liquidity of { bid : Scalar.Quantity.t; ask : Scalar.Quantity.t } + | Trade_liquidity of Scalar.Quantity.t + +let event_capacity state instrument quantity = + let* capacity = + Scalar.Quantity.bps_floor quantity ~bps:state.participation_bps + in + Scalar.Quantity.round_toward_zero_to_multiple capacity + ~multiple:instrument.Instrument.lot_size + +let validate_market_event instrument (event : Market_event.t) = + let tick = instrument.Instrument.tick_size in + let aligned = function + | Market_event.Quote { bid_price; ask_price; _ } -> + Scalar.Price.is_multiple bid_price ~tick + && Scalar.Price.is_multiple ask_price ~tick + | Trade { price; _ } -> Scalar.Price.is_multiple price ~tick + in + if not (Id.Instrument.equal instrument.id event.instrument_id) then + Error "execution instrument differs from the market event instrument" + else if not (aligned event.kind) then + Error "market event price is not aligned to the instrument tick size" + else Ok () + +let event_trigger order (event : Market_event.t) = + let observed_price = + match (event.kind, order.Order.request.side) with + | Market_event.Quote { ask_price; _ }, Order.Buy -> ask_price + | Quote { bid_price; _ }, Sell -> bid_price + | Trade { price; _ }, _ -> price + in + match (order.request.kind, order.request.side) with + | Order.Stop trigger, Buy | Stop_limit { trigger_price = trigger; _ }, Buy -> + Scalar.Price.compare observed_price trigger >= 0 + | Order.Stop trigger, Sell | Stop_limit { trigger_price = trigger; _ }, Sell + -> + Scalar.Price.compare observed_price trigger <= 0 + | (Market | Limit _), _ -> false + +let event_opportunity order (event : Market_event.t) liquidity = + match + (event.kind, liquidity, Order.effective_kind order, order.request.side) + with + | Quote { ask_price; _ }, Quote_liquidity { ask; _ }, Some Market, Buy -> + Some (ask_price, ask, Fee_schedule.Taker) + | Quote { bid_price; _ }, Quote_liquidity { bid; _ }, Some Market, Sell -> + Some (bid_price, bid, Fee_schedule.Taker) + | Quote { ask_price; _ }, Quote_liquidity { ask; _ }, Some (Limit limit), Buy + when Scalar.Price.compare ask_price limit <= 0 -> + Some (ask_price, ask, Fee_schedule.Taker) + | Quote { bid_price; _ }, Quote_liquidity { bid; _ }, Some (Limit limit), Sell + when Scalar.Price.compare bid_price limit >= 0 -> + Some (bid_price, bid, Fee_schedule.Taker) + | ( Trade { price; aggressor_side = Market_event.Sell; _ }, + Trade_liquidity quantity, + Some (Limit limit), + Buy ) + when Scalar.Price.compare price limit <= 0 -> + Some (price, quantity, Fee_schedule.Maker) + | ( Trade { price; aggressor_side = Market_event.Buy; _ }, + Trade_liquidity quantity, + Some (Limit limit), + Sell ) + when Scalar.Price.compare price limit >= 0 -> + Some (price, quantity, Fee_schedule.Maker) + | _ -> None + +let consume_observable side liquidity quantity = + match liquidity with + | Quote_liquidity { bid; ask } -> + if side = Order.Buy then + Result.map + (fun ask -> Quote_liquidity { bid; ask }) + (Scalar.Quantity.subtract ask quantity) + else + Result.map + (fun bid -> Quote_liquidity { bid; ask }) + (Scalar.Quantity.subtract bid quantity) + | Trade_liquidity available -> + Result.map + (fun value -> Trade_liquidity value) + (Scalar.Quantity.subtract available quantity) + +let start_slice_quote_trade state ~instruments ~oms + (market_slice : Market_slice.t) = + 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 prepare_event event = + match + Id.Instrument.Map.find_opt event.Market_event.instrument_id instrument_map + with + | None -> Error "market event refers to an unknown instrument" + | Some instrument -> + let* () = validate_market_event instrument event in + if + Ptime.compare event.event_at market_slice.start_at < 0 + || Ptime.compare event.event_at market_slice.end_at > 0 + || Ptime.compare event.received_at market_slice.received_at > 0 + then Error "market event falls outside its observable slice boundary" + else + let* liquidity = + match event.kind with + | Market_event.Quote { bid_quantity; ask_quantity; _ } -> + let* bid = event_capacity state instrument bid_quantity in + let* ask = event_capacity state instrument ask_quantity in + Ok (Quote_liquidity { bid; ask }) + | Trade { quantity; _ } -> + Result.map + (fun value -> Trade_liquidity value) + (event_capacity state instrument quantity) + in + Ok (event, instrument, liquidity) + in + let* events = + List.fold_right + (fun event result -> + let* prepared = prepare_event event in + let* remaining = result in + Ok (prepared :: remaining)) + market_slice.market_events (Ok []) + 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 + && + match order.trigger_state with + | Some (Order.Triggered { triggered_slice_sequence; _ }) -> + Int64.compare triggered_slice_sequence market_slice.slice_sequence + < 0 + | Some Order.Dormant | None -> true) + |> List.sort compare_execution_order + in + let order_ids = List.map (fun order -> order.Order.id) eligible in + let market_ioc_orders = + eligible + |> List.filter_map (fun order -> + if Order.is_ioc order && not (Order.is_dormant_stop order) then + Some order.Order.id + else None) + in + let rec make_events = function + | [] -> cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders)) + | (event, instrument, liquidity) :: remaining_events -> + make_orders event instrument liquidity order_ids remaining_events + and make_orders event instrument liquidity remaining remaining_events = + Cursor + (fun current_oms -> + match remaining with + | [] -> + let (Cursor next) = make_events remaining_events in + next current_oms + | order_id :: remaining_orders -> ( + match Oms.find current_oms order_id with + | None -> Error "eligible order disappeared during market replay" + | Some order when not (Order.is_active order) -> + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + | Some order + when not + (Id.Instrument.equal order.request.instrument_id + event.Market_event.instrument_id) -> + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + | Some order when Order.is_dormant_stop order -> + let continuation = + make_orders event instrument liquidity remaining_orders + remaining_events + in + if event_trigger order event then + Ok + (Triggered + ( order.id, + event.event_at, + market_slice.slice_sequence, + continuation )) + else + let (Cursor next) = continuation in + next current_oms + | Some order -> ( + match event_opportunity order event liquidity with + | None -> + let (Cursor next) = + make_orders event instrument liquidity remaining_orders + remaining_events + in + next current_oms + | Some (price, available, fee_liquidity) -> + let quantity = + Scalar.Quantity.minimum available + (Order.remaining_quantity order) + in + if + Scalar.Quantity.is_zero quantity + || Order.is_fok order + && Scalar.Quantity.compare quantity + (Order.remaining_quantity order) + < 0 + then + let (Cursor next) = + make_orders event instrument liquidity 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 observable 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* liquidity = + consume_observable order.request.side liquidity + applied_quantity + in + Ok + (make_orders event instrument liquidity + remaining_orders remaining_events) + in + Ok (Proposed (proposed, continue))))) + in + Ok (make_events 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 c625232..8ba8765 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -100,6 +100,13 @@ val start_slice_adverse_touch : Market_slice.t -> (cursor, string) result +val start_slice_quote_trade : + 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 11b13d5..ddd6154 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -37,6 +37,11 @@ module Completed_bar_adverse_touch_v1 = struct let start_slice = Execution.start_slice_adverse_touch end +module Quote_trade_v1 = struct + let name = "quote_trade_v1" + let start_slice = Execution.start_slice_quote_trade +end + let of_module model = model let name (module Model : S) = Model.name @@ -45,6 +50,7 @@ let builtins : t list = (module Completed_bar_v1); (module Completed_bar_next_open_v1); (module Completed_bar_adverse_touch_v1); + (module Quote_trade_v1); ] let supported = List.map name builtins @@ -54,7 +60,7 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ "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" ]; @@ -75,7 +81,7 @@ let conservative_contract = { version = "1"; previous_versions = []; - scenario_contract_versions = [ "13" ]; + scenario_contract_versions = [ "14"; "13" ]; required_fields = [ "version"; @@ -100,11 +106,34 @@ let conservative_contract = ]; } +let quote_trade_contract = + { + version = "1"; + previous_versions = []; + scenario_contract_versions = [ "14" ]; + required_fields = [ "version"; "participation_bps"; "fee_schedules" ]; + legacy_required_fields = []; + 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 = + `Assoc + [ + ( "participation_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ]; + } + 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 | 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 ce795ce..23c422c 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 "13" then + if String.equal contract_version "14" then + Engine.config_v14 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "13" then Engine.config_v13 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/market_event.ml b/lib/market_event.ml new file mode 100644 index 0000000..294a4b7 --- /dev/null +++ b/lib/market_event.ml @@ -0,0 +1,92 @@ +type aggressor_side = Buy | Sell | Unknown + +type kind = + | Quote of { + bid_price : Scalar.Price.t; + bid_quantity : Scalar.Quantity.t; + ask_price : Scalar.Price.t; + ask_quantity : Scalar.Quantity.t; + } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : aggressor_side; + } + +type t = { + instrument_id : Id.Instrument.t; + event_at : Ptime.t; + available_at : Ptime.t; + received_at : Ptime.t; + ingest_sequence : int64; + kind : kind; +} + +let validate_common ~event_at ~available_at ~received_at ~ingest_sequence = + if Int64.compare ingest_sequence 0L <= 0 then + Error "market event ingest sequence must be positive" + else if Ptime.compare available_at event_at < 0 then + Error "market event availability must not precede event time" + else if Ptime.compare received_at available_at < 0 then + Error "market event receipt must not precede availability" + else Ok () + +let quote ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~bid_price ~bid_quantity ~ask_price ~ask_quantity = + let ( let* ) result function_ = Result.bind result function_ in + let* () = + validate_common ~event_at ~available_at ~received_at ~ingest_sequence + in + if Scalar.Price.compare bid_price ask_price >= 0 then + Error "quote bid price must be below ask price" + else if + Scalar.Quantity.is_zero bid_quantity || Scalar.Quantity.is_zero ask_quantity + then Error "quote quantities must be positive" + else + Ok + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + kind = Quote { bid_price; bid_quantity; ask_price; ask_quantity }; + } + +let trade ~instrument_id ~event_at ~available_at ~received_at ~ingest_sequence + ~price ~quantity ~aggressor_side = + let ( let* ) result function_ = Result.bind result function_ in + let* () = + validate_common ~event_at ~available_at ~received_at ~ingest_sequence + in + if Scalar.Quantity.is_zero quantity then + Error "trade quantity must be positive" + else + Ok + { + instrument_id; + event_at; + available_at; + received_at; + ingest_sequence; + kind = Trade { price; quantity; aggressor_side }; + } + +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 aggressor_side_to_string = function + | Buy -> "buy" + | Sell -> "sell" + | Unknown -> "unknown" + +let aggressor_side_of_string = function + | "buy" -> Ok Buy + | "sell" -> Ok Sell + | "unknown" -> Ok Unknown + | _ -> Error "trade aggressor_side must be buy, sell, or unknown" diff --git a/lib/market_event.mli b/lib/market_event.mli new file mode 100644 index 0000000..d8a3287 --- /dev/null +++ b/lib/market_event.mli @@ -0,0 +1,52 @@ +(** Causally observable quote and trade events. *) + +type aggressor_side = Buy | Sell | Unknown + +type kind = + | Quote of { + bid_price : Scalar.Price.t; + bid_quantity : Scalar.Quantity.t; + ask_price : Scalar.Price.t; + ask_quantity : Scalar.Quantity.t; + } + | Trade of { + price : Scalar.Price.t; + quantity : Scalar.Quantity.t; + aggressor_side : 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; + kind : kind; +} + +val quote : + instrument_id:Id.Instrument.t -> + event_at:Ptime.t -> + available_at:Ptime.t -> + received_at:Ptime.t -> + ingest_sequence:int64 -> + bid_price:Scalar.Price.t -> + bid_quantity:Scalar.Quantity.t -> + ask_price:Scalar.Price.t -> + ask_quantity:Scalar.Quantity.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 -> + price:Scalar.Price.t -> + quantity:Scalar.Quantity.t -> + aggressor_side:aggressor_side -> + (t, string) result + +val compare_replay_order : t -> t -> int +val aggressor_side_to_string : aggressor_side -> string +val aggressor_side_of_string : string -> (aggressor_side, string) result diff --git a/lib/market_slice.ml b/lib/market_slice.ml index d50c61c..e9a1613 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -7,6 +7,7 @@ type t = { available_at : Ptime.t; received_at : Ptime.t; bars : Bar.t list; + market_events : Market_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -31,9 +32,10 @@ let fx_mark ~currency ~rate = let compare_bar left right = Id.Instrument.compare left.Bar.instrument_id right.Bar.instrument_id -let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at +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 = + ~cash_rate_observations ~settlement_failures ~lifecycle_events + ~market_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 @@ -123,6 +125,15 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at (String.equal left.Settlement.instruction_id right.instruction_id)) && unique_failure remaining in + let rec ordered_events = function + | [] | [ _ ] -> true + | left :: (right :: _ as remaining) -> + Market_event.compare_replay_order left right < 0 + && Int64.compare left.Market_event.ingest_sequence + right.Market_event.ingest_sequence + < 0 + && ordered_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" @@ -138,6 +149,10 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at Error "market slice cash rate currencies must be unique" else if not (unique_failure settlement_failures) then Error "market slice settlement failure instruction IDs must be unique" + else if not (ordered_events market_events) then + Error + "market events must be strictly ordered by availability, receipt, and \ + ingest sequence" else Ok { @@ -147,6 +162,7 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at available_at; received_at; bars; + market_events; fx_rates; corporate_actions; lifecycle_events; @@ -155,6 +171,13 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } +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 + ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations + ~settlement_failures ~lifecycle_events ~market_events:[] + let create_v13 = create_v12 let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at @@ -193,9 +216,10 @@ let compare_replay_order left right = let pp formatter state = Format.fprintf formatter - "slice[%Ld] bars=%d fx=%d actions=%d lifecycle=%d borrow=%d cash_rates=%d \ - failures=%d" + "slice[%Ld] bars=%d 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.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 6f08692..5785f03 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -12,6 +12,7 @@ type t = private { available_at : Ptime.t; received_at : Ptime.t; bars : Bar.t list; + market_events : Market_event.t list; fx_rates : fx_mark list; corporate_actions : Corporate_action.t list; lifecycle_events : Instrument_lifecycle.event list; @@ -88,6 +89,22 @@ val create_v13 : lifecycle_events:Instrument_lifecycle.event list -> (t, string) result +val create_v14 : + 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 -> + (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/replay.ml b/lib/replay.ml index 459c992..6807cda 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 "13" then + if String.equal contract_version "14" then + Engine.config_v14 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "13" then Engine.config_v13 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/scenario.ml b/lib/scenario.ml index 734e1bd..dcf86bc 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -554,8 +554,8 @@ 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 [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then - parse_v7_risk base_currency instruments json + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7" ] + then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json let parse_execution_values fields = @@ -803,8 +803,9 @@ 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 version "2" then - parse_execution_v2 instruments configuration + else if + String.equal model_name "quote_trade_v1" || String.equal version "2" + then parse_execution_v2 instruments configuration else parse_execution_values configuration in Ok (execution_model, execution) @@ -812,7 +813,7 @@ let parse_versioned_execution ~contract_version ~instruments json = let parse_execution ~contract_version ~instruments json = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "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 @@ -862,7 +863,7 @@ let parse_portfolio_intent ~name ~parse_target make json = let parse_submit_intent ~contract_version json = let versioned = - List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] + List.mem contract_version [ "14"; "13"; "12"; "11"; "10"; "9"; "8" ] in let* fields = object_fields ~name:"submit_order intent" @@ -1598,21 +1599,124 @@ let parse_cash_rate_observation json = Financing.cash_rate_observation ~currency ~effective_at ~credit_rate_bps ~debit_rate_bps +let parse_market_event json = + let* loose_fields = + match json with + | `Assoc fields -> Ok fields + | _ -> Error "market event must be a JSON object" + in + let* type_name = + Result.bind (field loose_fields "type") (string ~name:"market event type") + in + let common_fields = + [ + "type"; + "instrument_id"; + "event_at"; + "available_at"; + "received_at"; + "ingest_sequence"; + ] + in + let specific_fields = + match type_name with + | "quote" -> [ "bid_price"; "bid_quantity"; "ask_price"; "ask_quantity" ] + | "trade" -> [ "price"; "quantity"; "aggressor_side" ] + | _ -> [] + in + let* () = + if specific_fields = [] then + Error "market event type must be quote or trade" + else Ok () + in + let* fields = + object_fields + ~name:(type_name ^ " market event") + ~expected:(common_fields @ specific_fields) + json + in + let* instrument_id = + Result.bind + (field fields "instrument_id") + (parse_id Id.Instrument.of_string ~name:"market event instrument_id") + in + let* event_at = + Result.bind (field fields "event_at") + (parse_timestamp ~name:"market event_at") + in + let* available_at = + Result.bind + (field fields "available_at") + (parse_timestamp ~name:"market available_at") + in + let* received_at = + Result.bind + (field fields "received_at") + (parse_timestamp ~name:"market received_at") + in + let* ingest_sequence = + Result.bind + (field fields "ingest_sequence") + (parse_int64 ~name:"market ingest_sequence") + in + match type_name with + | "quote" -> + let* bid_price = + Result.bind (field fields "bid_price") (parse_price ~name:"bid_price") + in + let* bid_quantity = + Result.bind + (field fields "bid_quantity") + (parse_quantity ~name:"bid_quantity") + in + let* ask_price = + Result.bind (field fields "ask_price") (parse_price ~name:"ask_price") + in + let* ask_quantity = + Result.bind + (field fields "ask_quantity") + (parse_quantity ~name:"ask_quantity") + in + Market_event.quote ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~bid_price ~bid_quantity ~ask_price ~ask_quantity + | "trade" -> + let* price = + Result.bind (field fields "price") (parse_price ~name:"trade price") + in + let* quantity = + Result.bind (field fields "quantity") + (parse_quantity ~name:"trade quantity") + in + let* aggressor_side = + Result.bind + (Result.bind + (field fields "aggressor_side") + (string ~name:"aggressor_side")) + Market_event.aggressor_side_of_string + in + Market_event.trade ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~price ~quantity ~aggressor_side + | _ -> assert false + let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then [ "settlement_failures" ] else [] in let lifecycle_fields = - if List.mem contract_version [ "13"; "12" ] then [ "lifecycle_events" ] + if List.mem contract_version [ "14"; "13"; "12" ] then + [ "lifecycle_events" ] else [] in + let market_event_fields = + if String.equal contract_version "14" then [ "market_events" ] else [] + in let* fields = object_fields ~name:"market slice" ~expected: @@ -1626,7 +1730,8 @@ let parse_slice ~contract_version json = "fx_rates"; "corporate_actions"; ] - @ financing_fields @ settlement_fields @ lifecycle_fields) + @ financing_fields @ settlement_fields @ lifecycle_fields + @ market_event_fields) json in let* sequence_json = field fields "slice_sequence" in @@ -1648,7 +1753,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 [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1663,7 +1768,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 [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1672,20 +1777,32 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - if List.mem contract_version [ "13"; "12" ] then + if List.mem contract_version [ "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 - let create = - if String.equal contract_version "13" then Market_slice.create_v13 - else Market_slice.create_v12 - in - create ~slice_sequence ~start_at ~end_at ~available_at ~received_at - ~bars ~fx_rates ~corporate_actions ~borrow_observations - ~cash_rate_observations ~settlement_failures ~lifecycle_events + if String.equal contract_version "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 + else + let create = + if String.equal contract_version "13" then Market_slice.create_v13 + else Market_slice.create_v12 + in + create ~slice_sequence ~start_at ~end_at ~available_at ~received_at + ~bars ~fx_rates ~corporate_actions ~borrow_observations + ~cash_rate_observations ~settlement_failures ~lifecycle_events else Market_slice.create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations @@ -1730,7 +1847,8 @@ let construct_header ~root ~contract_path ~contract_version in let* initial_cash, initial_portfolio = if - List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state @@ -1800,7 +1918,7 @@ let construct_header ~root ~contract_path ~contract_version | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then Some financing else None in diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index 6888490..e85661d 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -70,7 +70,9 @@ let common ~root ~contract_version fields = let* run_id = field ~root fields "run_id" in let* base_currency = field ~root fields "base_currency" in let initial_field = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -79,19 +81,19 @@ let common ~root ~contract_version fields = let venue_calendars = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "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 [ "13"; "12"; "11"; "10" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then List.assoc_opt "financing" fields else None in let settlement = - if List.mem contract_version [ "13"; "12"; "11" ] then + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -124,12 +126,14 @@ let batch json = let calendar_fields = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -150,11 +154,12 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "13"; "12"; "11" ] then [ "settlement" ] + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + [ "settlement" ] else []) json in @@ -169,12 +174,14 @@ let stream_header ~contract_version json = let calendar_fields = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio" else "initial_cash" in @@ -192,11 +199,12 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + @ (if List.mem contract_version [ "14"; "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "13"; "12"; "11" ] then [ "settlement" ] + if List.mem contract_version [ "14"; "13"; "12"; "11" ] then + [ "settlement" ] else []) json in diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 527b0b8..45032db 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,7 +48,9 @@ let validate_venue_calendars ~root catalog venue_calendars = let header ~root ~contract_version ~base_currency ~initial_cash ~instruments ~venue_calendars ~max_internal_events = let* () = - if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + if + List.mem contract_version + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Ok () else Account.create ~base_currency ~initial_cash @@ -69,7 +71,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "14"; "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -89,7 +91,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments (child root (if List.mem contract_version - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "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" @@ -384,6 +386,39 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = bar.volume) market_slice.bars in + let market_events_valid = + List.for_all + (fun (event : Market_event.t) -> + match + Id.Instrument.Map.find_opt event.instrument_id instrument_map + with + | None -> false + | Some instrument -> + let prices, quantities = + match event.kind with + | Market_event.Quote + { bid_price; bid_quantity; ask_price; ask_quantity } -> + ( [ bid_price; ask_price ], + [ bid_quantity; ask_quantity ] ) + | Trade { price; quantity; _ } -> ([ price ], [ quantity ]) + in + List.for_all + (fun price -> + Scalar.Price.is_multiple price ~tick:instrument.tick_size) + prices + && List.for_all + (fun quantity -> + Scalar.Quantity.is_multiple quantity + ~lot:instrument.lot_size) + quantities + && 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) + market_slice.market_events + in if not (Id.Instrument.Set.equal catalog ids) then fail ~json_path:(child root "bars") "each market slice must contain every configured instrument" @@ -413,6 +448,11 @@ let validate_slices_at ~paths ~base_currency ~currencies ~instruments slices = else if not bars_aligned then fail ~json_path:(child root "bars") "market prices and volumes must align with instrument increments" + else if not market_events_valid then + fail + ~json_path:(child root "market_events") + "market events must be known, aligned, and observable within the \ + slice" else if Option.exists (fun sequence -> diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index ece0f9a..120f8cb 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 [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -253,8 +253,10 @@ let execution_to_yojson ~protocol_version model execution = ] in if - String.equal protocol_version "11" - && not (String.equal (Execution_model.name model) "completed_bar_v1") + List.mem protocol_version [ "12"; "11" ] + && List.mem + (Execution_model.name model) + [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] then let costs = Execution.cost_model execution |> Option.get in `Assoc @@ -288,7 +290,25 @@ let execution_to_yojson ~protocol_version model execution = ] ); ] ); ] - else if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7" ] then + else if + String.equal protocol_version "12" + && String.equal (Execution_model.name model) "quote_trade_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)) ); + ] ); + ] + else if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] then `Assoc [ ("model", string (Execution_model.name model)); @@ -327,6 +347,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "14" -> "12" | "13" -> "11" | "12" -> "10" | "11" -> "9" @@ -374,7 +395,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -387,14 +408,14 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "11"; "10"; "9"; "8" ] then + (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); - (if List.mem protocol_version [ "11"; "10"; "9" ] then + (if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -428,14 +449,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 [ "11"; "10"; "9"; "8" ] then + @ (if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8" ] then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if List.mem protocol_version [ "11"; "10"; "9" ] then + if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -455,7 +476,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if List.mem protocol_version [ "11"; "10"; "9" ] then + if List.mem protocol_version [ "12"; "11"; "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -538,7 +559,9 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] + (if + List.mem protocol_version + [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); @@ -551,7 +574,11 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if List.mem protocol_version [ "11"; "10" ] then + 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 + else if String.equal protocol_version "10" then Codec.market_slice_to_yojson_v12 market_slice else if String.equal protocol_version "9" then Codec.market_slice_to_yojson_v11 market_slice @@ -564,8 +591,8 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7" ] then - Codec.fill_to_yojson_v9 fill + if List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7" ] + then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] | Strategy.Order_updated order -> @@ -573,8 +600,9 @@ let event_to_yojson ~protocol_version = function [ ("type", string "order_updated"); ( "order", - if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then - Codec.order_to_yojson_v8 order + if + List.mem protocol_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] | Strategy.Intent_rejected reason -> @@ -661,7 +689,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "11" then "13" + (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" else if String.equal protocol_version "8" then "10" diff --git a/mkdocs.yml b/mkdocs.yml index 7c61eb4..81af8aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v13: contracts/v13/README.md + - Current v14: contracts/v14/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 v11: contracts/strategy/v11/README.md + - Current v12: contracts/strategy/v12/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 671c25d..5966154 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -98,3 +98,15 @@ compare_journal \ v13-fill-clipped \ contracts/v13/fixtures/fill-clipped.scenario.json \ contracts/v13/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v14-demo \ + contracts/v14/fixtures/demo.scenario.json \ + contracts/v14/fixtures/demo.journal.jsonl +compare_journal \ + v14-fill-clipped \ + contracts/v14/fixtures/fill-clipped.scenario.json \ + contracts/v14/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v14-quote-trade \ + contracts/v14/fixtures/quote-trade.scenario.json \ + contracts/v14/fixtures/quote-trade.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index f6acfa2..1e73038 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/v13/README.md", + "contracts/v14/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v11/README.md", + "contracts/strategy/v12/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 6607931..62fd115 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/v13/scenario.schema.json", - "share/trading_engine/contracts/v13/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v14/scenario.schema.json", + "share/trading_engine/contracts/v14/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/v13/fixtures/demo.scenario.json", + "contracts/v14/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/v13/fixtures/demo.scenario.json", - "contracts/strategy/v11/message.schema.json", + "contracts/v14/fixtures/demo.scenario.json", + "contracts/strategy/v12/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v13/scenario.schema.json", + "contracts/v14/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index 62a7360..9291ade 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":["13","12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["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"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["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":["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":["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}}}],"strategy_protocol_versions":["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":["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}} $ ../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 fa0aebb..2c57ecb 100644 --- a/test/dune +++ b/test/dune @@ -78,6 +78,17 @@ ../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/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -94,6 +105,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/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -112,6 +124,69 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (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) + (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}))) + +(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) + (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}))) + +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v14/scenario.schema.json + ../contracts/v14/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) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v14/scenario.schema.json} + %{dep:../contracts/v14/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}))) + (rule (alias runtest) (deps diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index fcac251..6c487f9 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -112,6 +112,7 @@ let capabilities_describe_execution_contracts () = "completed_bar_v1"; "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1"; + "quote_trade_v1"; ] names; let model = List.hd models in @@ -135,7 +136,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "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 83a09f0..e3a50fd 100644 --- a/test/test_domain.ml +++ b/test/test_domain.ml @@ -100,6 +100,104 @@ let market_slice_validation () = Alcotest.(check bool) "premature availability rejected" true (Result.is_error result) +let market_event_validation () = + let instrument_id = instrument_id "event-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 quote ?(ingest_sequence = 1L) ?(bid_price = "99") + ?(bid_quantity = quantity "1") ?(ask_price = "101") () = + T.Market_event.quote ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence ~bid_price:(price bid_price) ~bid_quantity + ~ask_price:(price ask_price) ~ask_quantity:(quantity "1") + in + Alcotest.(check bool) + "nonpositive ingest rejected" true + (Result.is_error (quote ~ingest_sequence:0L ())); + Alcotest.(check bool) + "crossed quote rejected" true + (Result.is_error (quote ~bid_price:"101" ~ask_price:"100" ())); + Alcotest.(check bool) + "zero displayed quantity rejected" true + (Result.is_error (quote ~bid_quantity:T.Scalar.Quantity.zero ())); + Alcotest.(check bool) + "zero ask quantity rejected" true + (Result.is_error + (T.Market_event.quote ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence:1L ~bid_price:(price "99") + ~bid_quantity:(quantity "1") ~ask_price:(price "101") + ~ask_quantity:T.Scalar.Quantity.zero)); + Alcotest.(check bool) + "receipt before availability rejected" true + (Result.is_error + (T.Market_event.trade ~instrument_id ~event_at ~available_at + ~received_at:event_at ~ingest_sequence:1L ~price:(price "100") + ~quantity:(quantity "1") ~aggressor_side:T.Market_event.Sell)); + Alcotest.(check bool) + "zero trade quantity rejected" true + (Result.is_error + (T.Market_event.trade ~instrument_id ~event_at ~available_at ~received_at + ~ingest_sequence:1L ~price:(price "100") + ~quantity:T.Scalar.Quantity.zero + ~aggressor_side:T.Market_event.Unknown)); + Alcotest.(check bool) + "availability before event rejected" true + (Result.is_error + (T.Market_event.trade ~instrument_id ~event_at + ~available_at:(timestamp "2026-01-03T14:29:59Z") + ~received_at ~ingest_sequence:1L ~price:(price "100") + ~quantity:(quantity "1") ~aggressor_side:T.Market_event.Buy)); + let first = quote ~ingest_sequence:2L () |> ok in + let second = quote ~ingest_sequence:1L () |> ok in + let later_receipt = + T.Market_event.quote ~instrument_id ~event_at ~available_at + ~received_at:(timestamp "2026-01-03T14:30:03Z") + ~ingest_sequence:3L ~bid_price:(price "99") ~bid_quantity:(quantity "1") + ~ask_price:(price "101") ~ask_quantity:(quantity "1") + |> ok + in + Alcotest.(check bool) + "receipt breaks replay-order tie" true + (T.Market_event.compare_replay_order first later_receipt < 0); + List.iter + (fun (wire, side) -> + Alcotest.(check string) + (wire ^ " side round trip") + wire + (T.Market_event.aggressor_side_of_string wire + |> ok |> T.Market_event.aggressor_side_to_string); + Alcotest.(check string) + (wire ^ " constructor rendering") + wire + (T.Market_event.aggressor_side_to_string side)) + [ + ("buy", T.Market_event.Buy); + ("sell", T.Market_event.Sell); + ("unknown", T.Market_event.Unknown); + ]; + Alcotest.(check bool) + "unknown aggressor spelling rejected" true + (Result.is_error (T.Market_event.aggressor_side_of_string "ambiguous")); + 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" + (Format.asprintf "%a" T.Market_slice.pp base); + Alcotest.(check bool) + "nonmonotonic ingest rejected" true + (Result.is_error + (T.Market_slice.create_v14 ~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:[ first; second ])) + let bar_validation_boundaries () = let instrument_id = instrument_id "bar-validation" in let create ?(open_price = "100") ?(high_price = "110") ?(low_price = "90") @@ -298,6 +396,7 @@ let tests = Alcotest.test_case "portfolio weight rounds toward zero" `Quick 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 "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 bcae7e6..377b8ea 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -37,6 +37,207 @@ let conservative_step start ?(kind = T.Order.Market) ?(side = T.Order.Buy) let cursor = start engine ~instruments:[ instrument () ] ~oms slice |> ok in T.Execution.next cursor ~oms |> ok +let market_event_time second = + timestamp (Printf.sprintf "2026-01-03T14:30:%02dZ" second) + +let quote_event ?(sequence = 1L) ?(second = 1) ?(bid = "99") + ?(bid_quantity = "5") ?(ask = "101") ?(ask_quantity = "5") () = + let event_at = market_event_time second in + T.Market_event.quote + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~bid_price:(price bid) + ~bid_quantity:(quantity bid_quantity) ~ask_price:(price ask) + ~ask_quantity:(quantity ask_quantity) + |> ok + +let trade_event ?(sequence = 2L) ?(second = 2) ?(price_value = "100") + ?(quantity_value = "5") ?(aggressor_side = T.Market_event.Unknown) () = + let event_at = market_event_time second in + T.Market_event.trade + ~instrument_id:(instrument_id "test-equity") + ~event_at ~available_at:event_at ~received_at:event_at + ~ingest_sequence:sequence ~price:(price price_value) + ~quantity:(quantity quantity_value) ~aggressor_side + |> ok + +let quote_trade_slice events = + let base = market_slice 2L in + T.Market_slice.create_v14 ~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:events + |> ok + +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 liquidity_name = function + | T.Fee_schedule.Maker -> "maker" + | Taker -> "taker" + +let quote_trade_step ?(kind = T.Order.Market) ?(side = T.Order.Buy) events = + let oms, _ = oms_with_order (request ~kind ~side ()) in + let cursor = + T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms (quote_trade_slice events) + |> ok + in + T.Execution.next cursor ~oms |> ok + +let quote_trade_consumes_displayed_liquidity () = + match quote_trade_step [ quote_event ~ask_quantity:"3" () ] with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "buy executes at displayed ask" + (price "101") proposal.price; + Alcotest.check quantity_testable "displayed size caps fill" (quantity "3") + proposal.quantity; + Alcotest.(check string) + "quote fill is taker" "taker" + (liquidity_name proposal.liquidity); + Alcotest.(check string) + "economic event time" "2026-01-03T14:30:01.000000Z" + (T.Codec.ptime_to_string proposal.executed_at) + | _ -> Alcotest.fail "marketable quote did not produce a fill" + +let quote_trade_passive_fills_require_aggressor_evidence () = + let limit = T.Order.Limit (price "100") in + let events = + [ + quote_event (); + trade_event ~price_value:"99" (); + trade_event ~sequence:3L ~second:3 ~price_value:"99" + ~aggressor_side:T.Market_event.Sell (); + ] + in + match quote_trade_step ~kind:limit events with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "passive fill uses observed trade" + (price "99") proposal.price; + Alcotest.(check string) + "trade fill is maker" "maker" + (liquidity_name proposal.liquidity); + Alcotest.(check string) + "unknown aggressor was skipped" "2026-01-03T14:30:03.000000Z" + (T.Codec.ptime_to_string proposal.executed_at) + | _ -> Alcotest.fail "qualified passive trade did not produce a fill" + +let quote_trade_sell_paths_use_bid_and_buy_aggressors () = + (match quote_trade_step ~side:T.Order.Sell [ quote_event ~bid:"99" () ] with + | T.Execution.Proposed (proposal, continue) -> ( + Alcotest.check price_testable "sell executes at displayed bid" + (price "99") proposal.price; + let cursor = continue proposal.quantity |> ok in + match T.Execution.next cursor ~oms:T.Oms.empty |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "consumed quote should finish") + | _ -> Alcotest.fail "sell quote did not produce a fill"); + let passive = T.Order.Limit (price "100") in + match + quote_trade_step ~kind:passive ~side:T.Order.Sell + [ trade_event ~price_value:"101" ~aggressor_side:T.Market_event.Buy () ] + with + | T.Execution.Proposed (proposal, continue) -> + Alcotest.check price_testable "passive sell uses trade price" + (price "101") proposal.price; + Alcotest.(check string) + "passive sell is maker" "maker" + (liquidity_name proposal.liquidity); + ignore (continue proposal.quantity |> ok) + | _ -> Alcotest.fail "buy-aggressor trade did not fill passive sell" + +let quote_trade_limits_fok_and_continuations () = + let marketable = T.Order.Limit (price "102") in + (match + quote_trade_step ~kind:marketable [ quote_event ~ask_quantity:"3" () ] + with + | T.Execution.Proposed (proposal, continue) -> + Alcotest.check quantity_testable "marketable limit uses displayed size" + (quantity "3") proposal.quantity; + Alcotest.(check bool) + "over-consumption rejected" true + (Result.is_error (continue (quantity "4"))); + Alcotest.(check bool) + "negative application rejected" true + (Result.is_error (continue (quantity "-1"))) + | _ -> Alcotest.fail "marketable limit did not execute"); + let oms, _ = + oms_with_order + (request_v8 ~kind:T.Order.Market ~time_in_force:T.Order.Fok ()) + in + let cursor = + T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ quote_event ~ask_quantity:"3" () ]) + |> ok + in + match T.Execution.next cursor ~oms |> ok with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "FOK order filled partial displayed liquidity" + +let quote_trade_stop_and_event_boundaries () = + let oms, order = + oms_with_order + (request_v8 + ~kind:(T.Order.Stop (price "100")) + ~time_in_force:T.Order.Gtc ()) + in + let cursor = + T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ quote_event ~ask:"101" () ]) + |> ok + in + (match T.Execution.next cursor ~oms |> ok with + | T.Execution.Triggered (order_id, triggered_at, 2L, _) -> + Alcotest.(check string) + "triggered order" + (T.Id.Order.to_string order.id) + (T.Id.Order.to_string order_id); + Alcotest.(check string) + "quote trigger uses event time" "2026-01-03T14:30:01.000000Z" + (T.Codec.ptime_to_string triggered_at) + | _ -> Alcotest.fail "stop was not triggered by observable quote"); + let other_event = + let event_at = market_event_time 1 in + T.Market_event.quote ~instrument_id:(instrument_id "other") ~event_at + ~available_at:event_at ~received_at:event_at ~ingest_sequence:1L + ~bid_price:(price "99") ~bid_quantity:(quantity "1") + ~ask_price:(price "101") ~ask_quantity:(quantity "1") + |> ok + in + Alcotest.(check bool) + "unknown event instrument rejected" true + (Result.is_error + (T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ other_event ]))); + let old_at = timestamp "2026-01-02T14:30:00Z" in + let old_event = + T.Market_event.trade + ~instrument_id:(instrument_id "test-equity") + ~event_at:old_at ~available_at:old_at ~received_at:old_at + ~ingest_sequence:1L ~price:(price "100") ~quantity:(quantity "1") + ~aggressor_side:T.Market_event.Unknown + |> ok + in + Alcotest.(check bool) + "event outside slice rejected" true + (Result.is_error + (T.Execution.start_slice_quote_trade (quote_trade_execution ()) + ~instruments:[ instrument () ] + ~oms + (quote_trade_slice [ old_event ]))) + let conservative_limit_models_diverge () = let engine = conservative_execution () in let limit = T.Order.Limit (price "100") in @@ -658,6 +859,16 @@ let incomplete_market_slice_returns_error () = let tests = [ + Alcotest.test_case "quote replay consumes displayed liquidity" `Quick + quote_trade_consumes_displayed_liquidity; + Alcotest.test_case "passive trade requires aggressor evidence" `Quick + quote_trade_passive_fills_require_aggressor_evidence; + Alcotest.test_case "quote replay sell paths" `Quick + quote_trade_sell_paths_use_bid_and_buy_aggressors; + Alcotest.test_case "quote replay limits, FOK, and continuations" `Quick + quote_trade_limits_fok_and_continuations; + Alcotest.test_case "quote replay stops and boundaries" `Quick + quote_trade_stop_and_event_boundaries; 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 76306b3..3c57b8f 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,12 +2,16 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v13/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v14/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/v13/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v14/fixtures/demo.scenario.jsonl" +let quote_trade_path = "../contracts/v14/fixtures/quote-trade.scenario.json" + +let quote_trade_stream_path = + "../contracts/v14/fixtures/quote-trade.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -75,7 +79,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_v13 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v14 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -89,13 +93,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:[] + ~lifecycle_events:[] ~market_events:[] |> ok in let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v13 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v14 market_slice); ("intents", `List []); ] in @@ -140,9 +144,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v13/scenario.schema.json"; - check_schema "../contracts/v13/scenario-stream.schema.json"; - check_schema "../contracts/v13/journal.schema.json" + check_schema "../contracts/v14/scenario.schema.json"; + check_schema "../contracts/v14/scenario-stream.schema.json"; + check_schema "../contracts/v14/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -203,7 +207,7 @@ let v12_distributions_and_lifecycle_parse () = |> ok in let market_slice = - T.Market_slice.create_v13 ~slice_sequence:1L + T.Market_slice.create_v14 ~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") @@ -245,6 +249,7 @@ let v12_distributions_and_lifecycle_parse () = reason = "acquisition"; }); ] + ~market_events:[] |> ok in let document = @@ -341,7 +346,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v13 market_slice + (T.Codec.market_slice_to_yojson_v14 market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -431,8 +436,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 13, 12, 11, \ - 10, 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 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" @@ -582,7 +587,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_v13 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v14 ~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)) @@ -595,8 +600,8 @@ let dense_schedule_document slice_count = ~fx_rates:[ fx_mark () ] ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] - ~settlement_failures:[] ~lifecycle_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v13) + ~settlement_failures:[] ~lifecycle_events:[] ~market_events:[] + |> ok |> T.Codec.market_slice_to_yojson_v14) in let schedule = List.init slice_count (fun offset -> @@ -1180,7 +1185,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v13/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v14/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -1208,7 +1213,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v13/fixtures/fill-clipped.scenario.json" + "../contracts/v14/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -1222,11 +1227,69 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v13/fixtures/fill-clipped.journal.jsonl" + "../contracts/v14/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual +let quote_trade_replay_is_causal_and_stream_equivalent () = + let document = + In_channel.with_open_bin quote_trade_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 batch_journal = + 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/v14/fixtures/quote-trade.journal.jsonl" In_channel.input_all + in + Alcotest.(check string) "quote/trade golden journal" golden batch_journal; + 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)) + "only aggressor-qualified trade liquidity fills" + [ "4@99@2026-02-03T14:33:00.000000Z"; "6@100@2026-02-03T14:34: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 quote_trade_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-quote-trade" ".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 quote_trade_stream_path |> ok + in + Alcotest.(check int64) "two streamed slices" 2L streamed.slice_count; + Alcotest.(check string) + "quote/trade 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 @@ -1569,6 +1632,8 @@ let tests = v3_replay_matches_frozen_golden_file; Alcotest.test_case "fill clipping fixture reconciles" `Quick 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 "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 a0fe99c..9ad4f27 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" "11" + "protocol version" "12" (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 "11"); + ("strategy_protocol_version", `String "12"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload);