diff --git a/CHANGELOG.md b/CHANGELOG.md index d64a099..24a4c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- 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. +- Publish scenario/journal contract v13 and external strategy protocol v11 while preserving v12 + and protocol v10 as frozen compatibility contracts. + - Add exact stock-dividend, rights, and spin-off distributions with explicit basis allocation, fractional rejection or cash-in-lieu policy, destination currency validation, target adjustment, and complete journal attribution. diff --git a/README.md b/README.md index 50dbf26..8b92f9e 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,10 @@ 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; v12 currently exposes `completed_bar_v1` configuration v2 + capability descriptors; v13 adds next-open and adverse-touch models while freezing + `completed_bar_v1` +- Tick-aligned fixed-spread and participation-impact execution costs with separate reference, + spread, impact, and final-price audit attribution - 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 @@ -91,7 +94,7 @@ Validate the included scenario with an in-memory replay: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v12/fixtures/demo.scenario.json \ + --input contracts/v13/fixtures/demo.scenario.json \ --validate-only ``` @@ -99,7 +102,7 @@ Run it and create a journal: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v12/fixtures/demo.scenario.json \ + --input contracts/v13/fixtures/demo.scenario.json \ --journal demo.journal.jsonl ``` @@ -107,7 +110,7 @@ For larger histories, validate and replay the equivalent stream one slice at a t ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/v12/fixtures/demo.scenario.jsonl \ + --input contracts/v13/fixtures/demo.scenario.jsonl \ --input-format jsonl \ --journal demo.journal.jsonl ``` @@ -116,7 +119,7 @@ Run an external strategy against an empty-schedule scenario: ```sh opam exec -- dune exec trading-engine -- \ - --input contracts/strategy/v10/fixtures/external.scenario.json \ + --input contracts/strategy/v11/fixtures/external.scenario.json \ --journal external.journal.jsonl \ --strategy-executable ./my-strategy \ --strategy-arg=config.toml \ @@ -231,19 +234,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 v12 and conformance fixtures](contracts/v12/README.md) +- [Current contract v13 and conformance fixtures](contracts/v13/README.md) - [Frozen contract v2](contracts/v2/README.md) - [Historical contract v1](contracts/v1/README.md) -- [Scenario JSON Schema](contracts/v12/scenario.schema.json) -- [Scenario stream record JSON Schema](contracts/v12/scenario-stream.schema.json) -- [Journal record JSON Schema](contracts/v12/journal.schema.json) -- [External strategy protocol v10](contracts/strategy/v10/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) - [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/v10/message.schema.json) -- [Strategy transcript JSON Schema](contracts/strategy/v10/transcript.schema.json) +- [Strategy message JSON Schema](contracts/strategy/v11/message.schema.json) +- [Strategy transcript JSON Schema](contracts/strategy/v11/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 c36acb5..a32b12a 100644 --- a/contracts/conformance/cases.json +++ b/contracts/conformance/cases.json @@ -1274,6 +1274,90 @@ }, "mutations": [], "schema_expectation": "accept" + }, + { + "name": "scenario-v13-valid", + "artifact": "scenario-v13", + "kind": "scenario", + "source": "v13/fixtures/demo.scenario.json", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "scenario-stream-v13-valid", + "artifact": "scenario-stream-v13", + "kind": "scenario_stream", + "source": "v13/fixtures/demo.scenario.jsonl", + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept" + }, + { + "name": "strategy-ready-valid-v11", + "artifact": "strategy-message-v11", + "instance": { + "strategy_protocol_version": "11", + "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-v11", + "artifact": "strategy-message-v11", + "instance": { + "strategy_protocol_version": "11", + "strategy_sequence": "2", + "message_type": "intents", + "payload": { "intents": [] } + }, + "mutations": [], + "schema_expectation": "accept", + "parser_expectation": "accept", + "parser_expected": "intents" + }, + { + "name": "strategy-error-valid-v11", + "artifact": "strategy-message-v11", + "instance": { + "strategy_protocol_version": "11", + "strategy_sequence": "7", + "message_type": "error", + "payload": { "message": "fixture failure" } + }, + "mutations": [], + "schema_expectation": "accept" + }, + { + "name": "strategy-v11-rejected-response-branch", + "artifact": "strategy-transcript-v11", + "instance": { + "strategy_diagnostic_version": "1", + "transcript_sequence": "2", + "record_type": "rejected_strategy_response", + "expected_strategy_sequence": "1", + "diagnostic": { + "diagnostic_version": "1", + "code": "strategy.protocol", + "phase": "strategy", + "message": "strategy initialization: invalid strategy response JSON", + "context": { "json_path": "$", "sequence": "1" }, + "cause": null + }, + "evidence": { + "encoding": "hex", + "prefix": "7b", + "observed_bytes": 1, + "truncated": false + } + }, + "mutations": [], + "schema_expectation": "accept" } ] } diff --git a/contracts/conformance/manifest.json b/contracts/conformance/manifest.json index e61644d..97576ee 100644 --- a/contracts/conformance/manifest.json +++ b/contracts/conformance/manifest.json @@ -806,6 +806,55 @@ "sources": [ { "path": "strategy/v10/fixtures/external.strategy.jsonl", "format": "jsonl" } ] + }, + { + "name": "scenario-v13", + "schema": "v13/scenario.schema.json", + "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" } + ] + }, + { + "name": "scenario-stream-v13", + "schema": "v13/scenario-stream.schema.json", + "version_field": "contract_version", + "version": "13", + "sources": [ + { "path": "v13/fixtures/demo.scenario.jsonl", "format": "jsonl" }, + { "path": "strategy/v11/fixtures/external.scenario.jsonl", "format": "jsonl" } + ] + }, + { + "name": "journal-v13", + "schema": "v13/journal.schema.json", + "version_field": "contract_version", + "version": "13", + "sources": [ + { "path": "v13/fixtures/demo.journal.jsonl", "format": "jsonl" }, + { "path": "v13/fixtures/fill-clipped.journal.jsonl", "format": "jsonl" } + ] + }, + { + "name": "strategy-message-v11", + "schema": "strategy/v11/message.schema.json", + "version_field": "strategy_protocol_version", + "version": "11", + "sources": [ + { "path": "strategy/v11/fixtures/external.strategy.jsonl", "format": "jsonl", "extract": ["message"] } + ] + }, + { + "name": "strategy-transcript-v11", + "schema": "strategy/v11/transcript.schema.json", + "version_field": "strategy_protocol_version", + "version": "11", + "sources": [ + { "path": "strategy/v11/fixtures/external.strategy.jsonl", "format": "jsonl" } + ] } ] } diff --git a/contracts/strategy/v11/README.md b/contracts/strategy/v11/README.md new file mode 100644 index 0000000..11d712c --- /dev/null +++ b/contracts/strategy/v11/README.md @@ -0,0 +1,59 @@ +# External strategy protocol v11 + +Version 11 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 v10 remains available for scenario contract v12; earlier versions retain their frozen +shapes. + +Every message repeats `strategy_protocol_version: "11"` 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 v13 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 v13 intent shapes. Market-slice events include lifecycle transitions and +the expanded corporate-action catalog. + +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/v11/dune b/contracts/strategy/v11/dune new file mode 100644 index 0000000..977dbb1 --- /dev/null +++ b/contracts/strategy/v11/dune @@ -0,0 +1,15 @@ +(install + (section share) + (package trading_engine) + (files + (message.schema.json as contracts/strategy/v11/message.schema.json) + (transcript.schema.json as contracts/strategy/v11/transcript.schema.json) + (fixtures/external.scenario.json + as + contracts/strategy/v11/fixtures/external.scenario.json) + (fixtures/external.scenario.jsonl + as + contracts/strategy/v11/fixtures/external.scenario.jsonl) + (fixtures/external.strategy.jsonl + as + contracts/strategy/v11/fixtures/external.strategy.jsonl))) diff --git a/contracts/strategy/v11/fixtures/external.scenario.json b/contracts/strategy/v11/fixtures/external.scenario.json new file mode 100644 index 0000000..d6f9bab --- /dev/null +++ b/contracts/strategy/v11/fixtures/external.scenario.json @@ -0,0 +1,304 @@ +{ + "contract_version": "13", + "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": [] + }, + { + "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": [] + } + ], + "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/v11/fixtures/external.scenario.jsonl b/contracts/strategy/v11/fixtures/external.scenario.jsonl new file mode 100644 index 0000000..c1ced72 --- /dev/null +++ b/contracts/strategy/v11/fixtures/external.scenario.jsonl @@ -0,0 +1,4 @@ +{"contract_version":"13","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":"13","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"13","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":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"13","payload":{"slice_count":"2"},"record_type":"scenario_end","scenario_sequence":"4"} diff --git a/contracts/strategy/v11/fixtures/external.strategy.jsonl b/contracts/strategy/v11/fixtures/external.strategy.jsonl new file mode 100644 index 0000000..712ebc6 --- /dev/null +++ b/contracts/strategy/v11/fixtures/external.strategy.jsonl @@ -0,0 +1,14 @@ +{"strategy_protocol_version":"11","transcript_sequence":"1","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","strategy_sequence":"1","message_type":"initialize","payload":{"engine_version":"1.0.0","scenario_contract_version":"13","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":"11","transcript_sequence":"2","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","strategy_sequence":"1","message_type":"ready","payload":{"strategy_name":"fixture-strategy","strategy_version":"1"}}} +{"strategy_protocol_version":"11","transcript_sequence":"3","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","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":[]}}}}} +{"strategy_protocol_version":"11","transcript_sequence":"4","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","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":"11","transcript_sequence":"5","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","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":"11","transcript_sequence":"6","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","strategy_sequence":"3","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"11","transcript_sequence":"7","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","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":"11","transcript_sequence":"8","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","strategy_sequence":"4","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"11","transcript_sequence":"9","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","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":"11","transcript_sequence":"10","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","strategy_sequence":"5","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"11","transcript_sequence":"11","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","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":[]}}}}} +{"strategy_protocol_version":"11","transcript_sequence":"12","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","strategy_sequence":"6","message_type":"intents","payload":{"intents":[]}}} +{"strategy_protocol_version":"11","transcript_sequence":"13","direction":"engine_to_strategy","message":{"strategy_protocol_version":"11","strategy_sequence":"7","message_type":"shutdown","payload":{}}} +{"strategy_protocol_version":"11","transcript_sequence":"14","direction":"strategy_to_engine","message":{"strategy_protocol_version":"11","strategy_sequence":"7","message_type":"stopped","payload":{}}} diff --git a/contracts/strategy/v11/message.schema.json b/contracts/strategy/v11/message.schema.json new file mode 100644 index 0000000..4d26106 --- /dev/null +++ b/contracts/strategy/v11/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/v11/message.schema.json", + "title": "Trading Engine external strategy protocol v11 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": "11" }, + "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": "13" }, + "scenario_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, + "run_id": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/identifier" }, + "initial_cash": { + "type": "array", + "minItems": 1, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/cashBalance" } + }, + "initial_portfolio": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/initialPortfolio" } + ] + }, + "instruments": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/instrument" } + }, + "venue_calendars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/venueCalendar" } + }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/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/v13/scenario.schema.json#/$defs/timestamp" }, + "portfolio": { "$ref": "#/$defs/portfolio" }, + "working_orders": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/journal.schema.json#/$defs/order" } + }, + "latest_bars": { + "type": "array", + "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/scenario.schema.json#/$defs/identifier" }, + "cash": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/signedDecimal" }, + "net_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/signedDecimal" }, + "long_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/unsignedDecimal" }, + "short_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/unsignedDecimal" }, + "gross_exposure": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/unsignedDecimal" }, + "equity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/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/v13/journal.schema.json#/$defs/groupExposure" } + } + } + }, + "optionalWeight": { + "oneOf": [ + { "type": "null" }, + { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/scenario.schema.json#/$defs/identifier" }, + "quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/signedDecimal" }, + "settled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/signedDecimal" }, + "unsettled_quantity": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/signedDecimal" }, + "mark": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/positiveDecimal" }, + "base_market_value": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/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/v13/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/v13/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/v13/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/v11/transcript.schema.json b/contracts/strategy/v11/transcript.schema.json new file mode 100644 index 0000000..bbbca4b --- /dev/null +++ b/contracts/strategy/v11/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/v11/transcript.schema.json", + "title": "Trading Engine external strategy protocol v11 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": "11" }, + "transcript_sequence": { "$ref": "#/$defs/canonicalSequence" }, + "direction": { + "enum": ["engine_to_strategy", "strategy_to_engine"] + }, + "message": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/strategy/v11/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/v13/README.md b/contracts/v13/README.md new file mode 100644 index 0000000..093283c --- /dev/null +++ b/contracts/v13/README.md @@ -0,0 +1,93 @@ +# Trading Engine contract v13 + +This directory is the authoritative v13 process and file contract shared by Trading Engine and its +clients. Versions 12 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 v13 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 v13 scenario, stream record, and journal record carries `"contract_version": "13"`. + +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 11 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. diff --git a/contracts/v13/dune b/contracts/v13/dune new file mode 100644 index 0000000..e8c2b5c --- /dev/null +++ b/contracts/v13/dune @@ -0,0 +1,18 @@ +(install + (section share) + (package trading_engine) + (files + (journal.schema.json as contracts/v13/journal.schema.json) + (scenario-stream.schema.json as contracts/v13/scenario-stream.schema.json) + (scenario.schema.json as contracts/v13/scenario.schema.json) + (fixtures/demo.journal.jsonl as contracts/v13/fixtures/demo.journal.jsonl) + (fixtures/demo.scenario.json as contracts/v13/fixtures/demo.scenario.json) + (fixtures/demo.scenario.jsonl + as + contracts/v13/fixtures/demo.scenario.jsonl) + (fixtures/fill-clipped.journal.jsonl + as + contracts/v13/fixtures/fill-clipped.journal.jsonl) + (fixtures/fill-clipped.scenario.json + as + contracts/v13/fixtures/fill-clipped.scenario.json))) diff --git a/contracts/v13/fixtures/demo.journal.jsonl b/contracts/v13/fixtures/demo.journal.jsonl new file mode 100644 index 0000000..4379ef7 --- /dev/null +++ b/contracts/v13/fixtures/demo.journal.jsonl @@ -0,0 +1,29 @@ +{"contract_version":"13","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":"e6d10a0b0f54a6ba6e1b37d0b35fbea5eaad7bf24fe36a6911ae44949ef9de9d","execution_model":"completed_bar_adverse_touch_v1"}} +{"contract_version":"13","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":"13","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":"13","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":[]}} +{"contract_version":"13","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":"13","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":"13","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":"13","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":"13","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":"13","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":[]}} +{"contract_version":"13","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":"13","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":"13","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":"13","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":"13","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":"13","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":"13","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":[]}} +{"contract_version":"13","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":"13","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":"13","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":"13","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":"13","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":"13","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":"13","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":[]}} +{"contract_version":"13","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":"13","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":"13","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":"13","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":"13","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":"e6d10a0b0f54a6ba6e1b37d0b35fbea5eaad7bf24fe36a6911ae44949ef9de9d","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/v13/fixtures/demo.scenario.json b/contracts/v13/fixtures/demo.scenario.json new file mode 100644 index 0000000..a5d6f3b --- /dev/null +++ b/contracts/v13/fixtures/demo.scenario.json @@ -0,0 +1,453 @@ +{ + "contract_version": "13", + "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": [] + }, + { + "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": [] + }, + { + "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": [] + }, + { + "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": [] + } + ], + "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/v13/fixtures/demo.scenario.jsonl b/contracts/v13/fixtures/demo.scenario.jsonl new file mode 100644 index 0000000..b42027a --- /dev/null +++ b/contracts/v13/fixtures/demo.scenario.jsonl @@ -0,0 +1,6 @@ +{"contract_version":"13","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":"13","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":[]}},"record_type":"market_slice","scenario_sequence":"2"} +{"contract_version":"13","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":[]}},"record_type":"market_slice","scenario_sequence":"3"} +{"contract_version":"13","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":[]}},"record_type":"market_slice","scenario_sequence":"4"} +{"contract_version":"13","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":[]}},"record_type":"market_slice","scenario_sequence":"5"} +{"contract_version":"13","payload":{"slice_count":"4"},"record_type":"scenario_end","scenario_sequence":"6"} diff --git a/contracts/v13/fixtures/fill-clipped.journal.jsonl b/contracts/v13/fixtures/fill-clipped.journal.jsonl new file mode 100644 index 0000000..995c19a --- /dev/null +++ b/contracts/v13/fixtures/fill-clipped.journal.jsonl @@ -0,0 +1,13 @@ +{"contract_version":"13","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":"a7bee278b2c1dd07734d797ca94eae4f4a8bba1dd892ac25df31a4fa504f7b75","execution_model":"completed_bar_v1"}} +{"contract_version":"13","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":"13","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":"13","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":[]}} +{"contract_version":"13","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":"13","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":"13","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":"13","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":[]}} +{"contract_version":"13","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":"13","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":"13","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":"13","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":"13","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":"a7bee278b2c1dd07734d797ca94eae4f4a8bba1dd892ac25df31a4fa504f7b75","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/v13/fixtures/fill-clipped.scenario.json b/contracts/v13/fixtures/fill-clipped.scenario.json new file mode 100644 index 0000000..9884ef4 --- /dev/null +++ b/contracts/v13/fixtures/fill-clipped.scenario.json @@ -0,0 +1,267 @@ +{ + "contract_version": "13", + "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": [] + }, + { + "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": [] + } + ], + "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/v13/journal.schema.json b/contracts/v13/journal.schema.json new file mode 100644 index 0000000..1c63368 --- /dev/null +++ b/contracts/v13/journal.schema.json @@ -0,0 +1,2413 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v13/journal.schema.json", + "title": "Trading Engine v13 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": "13" + }, + "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"] + } + } + }, + "initialState": { + "type": "object", + "additionalProperties": false, + "required": [ + "portfolio", + "valuation" + ], + "properties": { + "portfolio": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/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" + ], + "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/v13/scenario.schema.json#/$defs/borrowObservation" + } + }, + "cash_rate_observations": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/cashRateObservation" + } + }, + "settlement_failures": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/settlementFailure" + } + }, + "lifecycle_events": { + "type": "array", + "items": { + "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/lifecycleEvent" + } + } + } + }, + "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/v13/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/v13/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/v13/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/v13/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"] + }, + "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/v13/scenario-stream.schema.json b/contracts/v13/scenario-stream.schema.json new file mode 100644 index 0000000..22d7003 --- /dev/null +++ b/contracts/v13/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/v13/scenario-stream.schema.json", + "title": "Trading Engine v13 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": "13" }, + "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": "13" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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": "13" }, + "scenario_sequence": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/scenario.schema.json#/$defs/identifier" }, + "base_currency": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/identifier" }, + "initial_portfolio": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/initialPortfolio" }, + "instruments": { "type": "array", "minItems": 1, "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/instrument" } }, + "venue_calendars": { "type": "array", "minItems": 1, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/venueCalendar" } }, + "risk": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/risk" }, + "execution": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/execution" }, + "financing": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/financing" }, + "settlement": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/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/v13/scenario.schema.json#/$defs/marketSlice" }, + "intents": { "type": "array", "maxItems": 4096, "items": { "$ref": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json#/$defs/intent" } } + } + } + } +} diff --git a/contracts/v13/scenario.schema.json b/contracts/v13/scenario.schema.json new file mode 100644 index 0000000..0a072d1 --- /dev/null +++ b/contracts/v13/scenario.schema.json @@ -0,0 +1,713 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/fallblu/trading-engine/contracts/v13/scenario.schema.json", + "title": "Trading Engine v13 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": "13" }, + "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" } + } + } + ] + }, + "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" } + } + }, + "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", "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" } }, + "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" }] } + } + }, + "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 b93d25d..fdfddd4 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/v12/README.md) remain authoritative for process boundaries. +files under [Contracts](../contracts/v13/README.md) remain authoritative for process boundaries. diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index b635fb7..4c10789 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -21,7 +21,7 @@ Every runtime cell replays the frozen v3 demo, v5 demo, and v5 risk-limited fill canonical fixtures. Standard output and standard error are captured separately because human diagnostics may contain platform-specific paths or process details and are not part of the journal contract. -The full test suite additionally validates and replays the current v12 batch, stream, journal, and +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. Coverage runs once in the exact locked Ubuntu environment. The required Persistra job also runs diff --git a/docs/execution-model.md b/docs/execution-model.md index e20c710..6a9e18f 100644 --- a/docs/execution-model.md +++ b/docs/execution-model.md @@ -1,11 +1,12 @@ # Execution model The engine selects a compiled execution module by the scenario's stable `execution.model` name. -Contract v12 advertises and accepts `completed_bar_v1`; embedders can inject another module through +Contract v13 advertises `completed_bar_v1`, `completed_bar_next_open_v1`, and +`completed_bar_adverse_touch_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 v12 envelope separates selection from +Each compiled model owns a strict configuration contract. The v13 envelope separates selection from model-specific parameters: ```json @@ -45,6 +46,11 @@ therefore reject incompatible scenarios without guessing from a shared execution The completed-bar model consumes synchronized slices of OHLCV bars. Every slice contains exactly one bar for each configured instrument and produces one matching batch and one closing valuation. +The conservative models use strict configuration version `"1"`. Both require `spread_model` with +`model: "fixed_half_spread_v1"` and `half_spread_bps`, plus `impact_model` with +`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. + ## Eligibility An order records the slice after which it is eligible. The matcher requires: @@ -104,9 +110,20 @@ For a buy limit `L`: 3. Otherwise, do not fill. Sell limits use the symmetric open/high rule. Limit remainders remain GTC. The open rule gives -deterministic gap improvement. The touch rule is optimistic because completed bars contain no +deterministic gap improvement. The frozen `completed_bar_v1` touch rule is optimistic because completed bars contain no queue, path, or available-size evidence at the limit. +`completed_bar_next_open_v1` fills a limit only at a later marketable open. +`completed_bar_adverse_touch_v1` additionally permits maker fills after the completed bar trades +through the limit by at least one instrument tick. Its pre-cost reference is that one-tick adverse +price. A mere touch does not fill. + +For both conservative models, fixed half-spread and participation-linear impact are rounded away +from the reference price to whole instrument ticks. Buy adjustments add and sell adjustments +subtract. A cost-adjusted price that would violate a limit is ineligible. Before each fill the +engine emits `execution_price_selected`, attributing reference price, spread adjustment, impact +adjustment, and final executable price; the fill causally references that event. + ## Capacity and priority Missing volume means unlimited simulated capacity. Otherwise: diff --git a/docs/persistra.md b/docs/persistra.md index 285b155..7a23c24 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 v12 while retaining v11 through v3 and +structural checks. The engine advertises current contract v13 while retaining v12 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 v10](../contracts/strategy/v10/README.md). Persistra's host turns protocol +[strategy protocol v11](../contracts/strategy/v11/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 v12 contract may +- **Scenario:** Frozen scenario and stream artifacts do not change. The current v13 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 v8; a host must complete its exact initialization, event, + external boundary is strategy v11; 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 663ccf6..8111c8f 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 v12 demo](../contracts/v12/fixtures/demo.scenario.json) as the canonical complete example. -The [scenario JSON Schema](../contracts/v12/scenario.schema.json) provides structural validation. +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. 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/v12/scenario-stream.schema.json) validates each line, -and [the v12 stream fixture](../contracts/v12/fixtures/demo.scenario.jsonl) is the canonical example. +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 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; v12 is `"12"` | +| `contract_version` | Required string identifying this file contract; v13 is `"13"` | | `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,6 +130,18 @@ 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 +`completed_bar_adverse_touch_v1`, each with strict configuration version `"1"`. They retain +`participation_bps` and `fee_schedules`, and additionally require: + +- `spread_model`: `fixed_half_spread_v1` with `half_spread_bps` from 0 through 10,000. +- `impact_model`: `linear_participation_v1` with `coefficient_bps` from 0 through 10,000 and + `missing_volume_policy` set to `reject` or `zero_impact`. + +The next-open model does not infer intrabar limit fills. The adverse-touch model requires a +one-tick trade-through. Both round price costs away from the reference to the instrument tick and +journal reference, spread, impact, and final executable prices separately. + ## Schedule and intents Schedule entries are positive, strictly increasing, and anchored to existing slices: @@ -222,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 v12 `settlement` object selects `total_cash` or `settled_cash` buying power and +The v13 `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 @@ -255,7 +267,7 @@ than the next slice `start_at`. ## Audit journal -The [journal JSON Schema](../contracts/v12/journal.schema.json) validates each JSON Lines record. +The [journal JSON Schema](../contracts/v13/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/audit.ml b/lib/audit.ml index a57d669..0734f09 100644 --- a/lib/audit.ml +++ b/lib/audit.ml @@ -63,6 +63,12 @@ type event = cash_amount : Scalar.Money.t; } | Order_adjusted of { order : Order.t; action_id : Id.Corporate_action.t } + | Execution_price_selected of { + order_id : Id.Order.t; + instrument_id : Id.Instrument.t; + side : Order.side; + attribution : Execution.price_attribution; + } | Fill_applied of Fill.t | Settlement_instruction_created of Settlement.instruction | Settlement_completed of Settlement.instruction @@ -188,6 +194,7 @@ let event_name = function | Distribution_applied _ -> "distribution_applied" | Lifecycle_applied _ -> "lifecycle_applied" | Order_adjusted _ -> "order_adjusted" + | Execution_price_selected _ -> "execution_price_selected" | Fill_applied _ -> "fill_applied" | Settlement_instruction_created _ -> "settlement_instruction_created" | Settlement_completed _ -> "settlement_completed" diff --git a/lib/audit.mli b/lib/audit.mli index 6866ce7..30328af 100644 --- a/lib/audit.mli +++ b/lib/audit.mli @@ -65,6 +65,12 @@ type event = cash_amount : Scalar.Money.t; } | Order_adjusted of { order : Order.t; action_id : Id.Corporate_action.t } + | Execution_price_selected of { + order_id : Id.Order.t; + instrument_id : Id.Instrument.t; + side : Order.side; + attribution : Execution.price_attribution; + } | Fill_applied of Fill.t | Settlement_instruction_created of Settlement.instruction | Settlement_completed of Settlement.instruction diff --git a/lib/codec.ml b/lib/codec.ml index 10e85c3..98d2c67 100644 --- a/lib/codec.ml +++ b/lib/codec.ml @@ -375,9 +375,9 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = ); ] |> function - | `Assoc fields when List.mem contract_version [ "12"; "11"; "10" ] -> + | `Assoc fields when List.mem contract_version [ "13"; "12"; "11"; "10" ] -> let settlement = - if List.mem contract_version [ "12"; "11" ] then + if List.mem contract_version [ "13"; "12"; "11" ] then [ ( "settlement_failures", `List @@ -387,7 +387,7 @@ let versioned_market_slice_to_yojson ~contract_version market_slice = else [] in let lifecycle = - if String.equal contract_version "12" then + if List.mem contract_version [ "13"; "12" ] then [ ( "lifecycle_events", `List @@ -423,6 +423,9 @@ let market_slice_to_yojson_v11 market_slice = let market_slice_to_yojson_v12 market_slice = versioned_market_slice_to_yojson ~contract_version:"12" market_slice +let market_slice_to_yojson_v13 market_slice = + versioned_market_slice_to_yojson ~contract_version:"13" market_slice + let request_fields request = let kind, limit_price = match request.Order.kind with @@ -526,7 +529,7 @@ let order_to_yojson_v8 order = ]) let versioned_order_to_yojson ~contract_version order = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then order_to_yojson_v8 order else order_to_yojson order @@ -724,7 +727,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "cash_balances", `List (List.map - (if List.mem contract_version [ "12"; "11" ] then + (if List.mem contract_version [ "13"; "12"; "11" ] then cash_attribution_to_yojson_v11 else if String.equal contract_version "10" then cash_attribution_to_yojson_v10 @@ -733,7 +736,7 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = ( "positions", `List (List.map - (if List.mem contract_version [ "12"; "11" ] then + (if List.mem contract_version [ "13"; "12"; "11" ] then position_attribution_to_yojson_v11 else if String.equal contract_version "9" @@ -743,14 +746,15 @@ let account_valuation_to_yojson ?(contract_version = "8") valuation = valuation.positions) ); ] |> function - | `Assoc fields when List.mem contract_version [ "12"; "11"; "10"; "9" ] -> + | `Assoc fields when List.mem contract_version [ "13"; "12"; "11"; "10"; "9" ] + -> let financing = - if List.mem contract_version [ "12"; "11"; "10" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10" ] then [ ("cash_interest", money valuation.Account.cash_interest) ] else [] in let settlement = - if List.mem contract_version [ "12"; "11" ] then + if List.mem contract_version [ "13"; "12"; "11" ] then [ ("settled_cash", money valuation.Account.settled_cash); ("unsettled_cash", money valuation.unsettled_cash); @@ -797,7 +801,7 @@ 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 [ "12"; "11"; "10"; "9"; "8" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] then fields @ [ ( "group_exposures", @@ -925,8 +929,20 @@ let payload_to_yojson ~contract_version = function ("order", versioned_order_to_yojson ~contract_version order); ("action_id", string (Id.Corporate_action.to_string action_id)); ] + | Audit.Execution_price_selected + { order_id = id; instrument_id = instrument; side; attribution } -> + `Assoc + [ + ("order_id", order_id id); + ("instrument_id", instrument_id instrument); + ("side", string (Order.side_to_string side)); + ("reference_price", price attribution.Execution.reference_price); + ("spread_adjustment", money attribution.spread_adjustment); + ("impact_adjustment", money attribution.impact_adjustment); + ("final_price", price attribution.final_price); + ] | Audit.Fill_applied fill -> - if List.mem contract_version [ "12"; "11"; "10"; "9" ] then + if List.mem contract_version [ "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 497651a..0b10ce3 100644 --- a/lib/codec.mli +++ b/lib/codec.mli @@ -7,6 +7,7 @@ val market_slice_to_yojson : Market_slice.t -> Yojson.Safe.t 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 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 0bad3aa..6f14005 100644 --- a/lib/contract.ml +++ b/lib/contract.ml @@ -1,11 +1,12 @@ -let version = "12" -let previous_version = "11" +let version = "13" +let previous_version = "12" let legacy_journal_version = "3" let supported_versions = [ version; previous_version; + "11"; "10"; "9"; "8"; @@ -17,8 +18,8 @@ let supported_versions = ] let is_supported version = List.mem version supported_versions -let strategy_protocol_version = "10" -let previous_strategy_protocol_version = "9" +let strategy_protocol_version = "11" +let previous_strategy_protocol_version = "10" let engine_version = "1.0.0" let strings values = `List (List.map (fun value -> `String value) values) @@ -37,6 +38,7 @@ let capabilities_to_yojson () = [ strategy_protocol_version; previous_strategy_protocol_version; + "9"; "8"; "7"; "6"; diff --git a/lib/engine.ml b/lib/engine.ml index f1e69fc..dfe9c08 100644 --- a/lib/engine.ml +++ b/lib/engine.ml @@ -15,6 +15,18 @@ let make_config ~venue_calendars ~contract_version ~risk ~execution_model ~execution ~financing ~settlement ~max_internal_events = if not (Contract.is_supported contract_version) then Error "engine contract version is unsupported" + else if + List.mem (Execution_model.name execution_model) Execution_model.supported + && not (Execution_model.supports_contract execution_model contract_version) + then Error "execution model does not support the engine contract version" + else if + String.equal (Execution_model.name execution_model) "completed_bar_v1" + && Option.is_some (Execution.cost_model execution) + || List.mem + (Execution_model.name execution_model) + [ "completed_bar_next_open_v1"; "completed_bar_adverse_touch_v1" ] + && Option.is_none (Execution.cost_model execution) + then Error "execution model and pricing configuration are incompatible" else if max_internal_events <= 0 then Error "maximum internal events must be positive" else if max_internal_events > Resource_limits.internal_events then @@ -56,6 +68,7 @@ let config_v11 ~contract_version ~risk ~venue_calendars ~execution_model ~max_internal_events let config_v12 = config_v11 +let config_v13 = config_v12 let valid_sha256 value = String.length value = 64 @@ -1907,6 +1920,22 @@ module Interactive = struct with | None -> Error "execution order refers to an unknown instrument" | Some instrument -> ( + let* reduction = + match proposed.Execution.price_attribution with + | None -> Ok reduction + | Some attribution -> + let* reduction, price_event_id = + emit_with_id reduction + (Audit.Execution_price_selected + { + order_id = order.id; + instrument_id = order.request.instrument_id; + side = order.request.side; + attribution; + }) + in + Ok (with_causes reduction [ price_event_id ]) + in let id = fill_id reduction.state in match increment_fill_number reduction.state with | Error _ as error -> error diff --git a/lib/engine.mli b/lib/engine.mli index 50c9eaa..2cd959b 100644 --- a/lib/engine.mli +++ b/lib/engine.mli @@ -51,6 +51,17 @@ val config_v12 : max_internal_events:int -> (config, string) result +val config_v13 : + 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 7291282..7a4d04c 100644 --- a/lib/execution.ml +++ b/lib/execution.ml @@ -2,7 +2,26 @@ type fee_configuration = | Legacy of { fixed_fee : Scalar.Money.t; fee_bps : int } | Schedules of Fee_schedule.t Id.Instrument.Map.t -type t = { participation_bps : int; fee_configuration : fee_configuration } +type missing_volume_policy = Reject_missing_volume | Zero_impact + +type cost_model = { + half_spread_bps : int; + impact_coefficient_bps : int; + missing_volume_policy : missing_volume_policy; +} + +type t = { + participation_bps : int; + fee_configuration : fee_configuration; + cost_model : cost_model option; +} + +type price_attribution = { + reference_price : Scalar.Price.t; + spread_adjustment : Scalar.Money.t; + impact_adjustment : Scalar.Money.t; + final_price : Scalar.Price.t; +} type proposed_fill = { order_id : Id.Order.t; @@ -12,6 +31,7 @@ type proposed_fill = { fee_components : Fee_schedule.calculated_component list; liquidity : Fee_schedule.liquidity; executed_at : Ptime.t; + price_attribution : price_attribution option; } type match_result = { @@ -29,6 +49,9 @@ and step = | Triggered of Id.Order.t * Ptime.t * int64 * cursor | Proposed of proposed_fill * (Scalar.Quantity.t -> (cursor, string) result) +let ( let* ) result function_ = + match result with Ok value -> function_ value | Error _ as error -> error + let cursor next = Cursor (fun oms -> next ~oms) let create ~participation_bps ~fixed_fee ~fee_bps = @@ -39,7 +62,12 @@ let create ~participation_bps ~fixed_fee ~fee_bps = else if fee_bps < 0 || fee_bps > 10_000 then Error "fee basis points must be between 0 and 10000" else - Ok { participation_bps; fee_configuration = Legacy { fixed_fee; fee_bps } } + Ok + { + participation_bps; + fee_configuration = Legacy { fixed_fee; fee_bps }; + cost_model = None; + } let create_v2 ~participation_bps ~fee_schedules = if participation_bps < 0 || participation_bps > 10_000 then @@ -59,9 +87,30 @@ let create_v2 ~participation_bps ~fee_schedules = in Result.map (fun schedules -> - { participation_bps; fee_configuration = Schedules schedules }) + { + participation_bps; + fee_configuration = Schedules schedules; + cost_model = None; + }) (List.fold_left add (Ok Id.Instrument.Map.empty) fee_schedules) +let create_conservative ~participation_bps ~fee_schedules ~half_spread_bps + ~impact_coefficient_bps ~missing_volume_policy = + if half_spread_bps < 0 || half_spread_bps > 10_000 then + Error "half-spread basis points must be between 0 and 10000" + else if impact_coefficient_bps < 0 || impact_coefficient_bps > 10_000 then + Error "impact coefficient basis points must be between 0 and 10000" + else + Result.map + (fun state -> + { + state with + cost_model = + Some + { half_spread_bps; impact_coefficient_bps; missing_volume_policy }; + }) + (create_v2 ~participation_bps ~fee_schedules) + let participation_bps state = state.participation_bps let fixed_fee state = @@ -79,6 +128,8 @@ let fee_schedules state = | Legacy _ -> [] | Schedules schedules -> Id.Instrument.Map.bindings schedules |> List.map snd +let cost_model state = state.cost_model + let calculate_fee state ~instrument ~notional ~quantity ~liquidity ~fx_rates = match state.fee_configuration with | Legacy { fixed_fee; fee_bps } -> @@ -97,7 +148,19 @@ let calculate_fee state ~instrument ~notional ~quantity ~liquidity ~fx_rates = ~quote_currency:instrument.quote_currency ~notional ~quantity ~liquidity ~fx_rates) -let execution_price order market_slice bar = +type limit_fill_policy = Optimistic_touch | Next_open_only | Adverse_touch + +let checked_price_micros value = + if Z.fits_int64 value then Scalar.Price.of_micros (Z.to_int64 value) + else Error "execution price overflow" + +let adverse_reference side limit tick = + let limit = Z.of_int64 (Scalar.Price.to_micros limit) in + let tick = Z.of_int64 (Scalar.Price.to_micros tick) in + checked_price_micros + (match side with Order.Buy -> Z.sub limit tick | Sell -> Z.add limit tick) + +let execution_reference policy instrument order market_slice bar = match Order.effective_kind order with | None -> None | Some Order.Market -> @@ -107,20 +170,126 @@ let execution_price order market_slice bar = Fee_schedule.Taker ) | Some (Order.Limit limit) -> ( match order.request.side with - | Order.Buy -> + | Order.Buy -> ( if Scalar.Price.compare bar.open_price limit <= 0 then Some (bar.open_price, market_slice.start_at, Fee_schedule.Taker) - else if Scalar.Price.compare bar.low_price limit <= 0 then - Some (limit, market_slice.end_at, Fee_schedule.Maker) - else None - | Order.Sell -> + else + match policy with + | Optimistic_touch -> + if Scalar.Price.compare bar.low_price limit <= 0 then + Some (limit, market_slice.end_at, Fee_schedule.Maker) + else None + | Next_open_only -> None + | Adverse_touch -> ( + match + adverse_reference Order.Buy limit + instrument.Instrument.tick_size + with + | Error _ -> None + | Ok reference -> + if Scalar.Price.compare bar.low_price reference <= 0 then + Some (reference, market_slice.end_at, Fee_schedule.Maker) + else None)) + | Order.Sell -> ( if Scalar.Price.compare bar.open_price limit >= 0 then Some (bar.open_price, market_slice.start_at, Fee_schedule.Taker) - else if Scalar.Price.compare bar.high_price limit >= 0 then - Some (limit, market_slice.end_at, Fee_schedule.Maker) - else None) + else + match policy with + | Optimistic_touch -> + if Scalar.Price.compare bar.high_price limit >= 0 then + Some (limit, market_slice.end_at, Fee_schedule.Maker) + else None + | Next_open_only -> None + | Adverse_touch -> ( + match + adverse_reference Order.Sell limit instrument.tick_size + with + | Error _ -> None + | Ok reference -> + if Scalar.Price.compare bar.high_price reference >= 0 then + Some (reference, market_slice.end_at, Fee_schedule.Maker) + else None))) | Some (Order.Stop _ | Order.Stop_limit _) -> None +let ceil_div numerator denominator = + if Z.equal numerator Z.zero then Z.zero + else Z.div (Z.add numerator (Z.pred denominator)) denominator + +let round_up_to_tick value tick = Z.mul (ceil_div value tick) tick + +let price_adjustment reference bps = + ceil_div + (Z.mul (Z.of_int64 (Scalar.Price.to_micros reference)) (Z.of_int bps)) + (Z.of_int 10_000) + +let impact_adjustment reference coefficient quantity volume = + ceil_div + (Z.mul + (Z.mul + (Z.of_int64 (Scalar.Price.to_micros reference)) + (Z.of_int coefficient)) + (Z.of_int64 (Scalar.Quantity.to_micros quantity))) + (Z.mul (Z.of_int 10_000) (Z.of_int64 (Scalar.Quantity.to_micros volume))) + +let apply_cost_model state instrument order bar quantity reference = + match state.cost_model with + | None -> Ok (Some (reference, None)) + | Some model -> + let tick = + Z.of_int64 (Scalar.Price.to_micros instrument.Instrument.tick_size) + in + let spread = + price_adjustment reference model.half_spread_bps |> fun value -> + round_up_to_tick value tick + in + let* impact = + if model.impact_coefficient_bps = 0 then Ok Z.zero + else + match bar.Bar.volume with + | Some volume when not (Scalar.Quantity.is_zero volume) -> + Ok + ( impact_adjustment reference model.impact_coefficient_bps + quantity volume + |> fun value -> round_up_to_tick value tick ) + | Some _ | None -> ( + match model.missing_volume_policy with + | Reject_missing_volume -> + Error "impact model requires completed-bar volume" + | Zero_impact -> Ok Z.zero) + in + let adjustment = Z.add spread impact in + let reference_micros = Z.of_int64 (Scalar.Price.to_micros reference) in + let final_micros = + match order.Order.request.side with + | Buy -> Z.add reference_micros adjustment + | Sell -> Z.sub reference_micros adjustment + in + let* final_price = checked_price_micros final_micros in + let respects_limit = + match Order.effective_kind order with + | Some (Order.Limit limit) -> ( + match order.request.side with + | Buy -> Scalar.Price.compare final_price limit <= 0 + | Sell -> Scalar.Price.compare final_price limit >= 0) + | Some (Market | Stop _ | Stop_limit _) | None -> true + in + if not respects_limit then Ok None + else if not (Z.fits_int64 spread && Z.fits_int64 impact) then + Error "execution price adjustment overflow" + else + Ok + (Some + ( final_price, + Some + { + reference_price = reference; + spread_adjustment = + Scalar.Money.of_micros (Z.to_int64 spread); + impact_adjustment = + Scalar.Money.of_micros (Z.to_int64 impact); + final_price; + } )) + let stop_trigger order market_slice bar = match (order.Order.request.kind, order.request.side) with | Order.Stop trigger_price, Order.Buy @@ -199,7 +368,8 @@ let compare_execution_order left right = in if sequence <> 0 then sequence else Id.Order.compare left.id right.id -let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = +let start_slice_with_policy policy state ~instruments ~oms + (market_slice : Market_slice.t) = let ( let* ) result function_ = match result with Ok value -> function_ value | Error _ as error -> error in @@ -280,11 +450,13 @@ let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = market_slice.slice_sequence, make_cursor capacities remaining )) else - match execution_price order market_slice bar with + match + execution_reference policy instrument order market_slice bar + with | None -> let (Cursor next) = make_cursor capacities remaining in next current_oms - | Some (price, executed_at, liquidity) -> + | Some (reference_price, executed_at, liquidity) -> ( let quantity = available_quantity capacity (Order.remaining_quantity order) in @@ -298,53 +470,74 @@ let start_slice state ~instruments ~oms (market_slice : Market_slice.t) = let (Cursor next) = make_cursor capacities remaining in next current_oms else - let* notional = Scalar.Money.notional price quantity in - let* fee_components, fee = - calculate_fee state ~instrument ~notional ~quantity ~liquidity - ~fx_rates: - (List.map - (fun mark -> (mark.Market_slice.currency, mark.rate)) - market_slice.fx_rates) + let* priced = + apply_cost_model state instrument order bar quantity + reference_price in - let proposed = - { - order_id = order.id; - quantity; - price; - fee; - fee_components; - liquidity; - executed_at; - } - in - let continue applied_quantity = - if - Scalar.Quantity.compare applied_quantity - Scalar.Quantity.zero - < 0 - then Error "applied fill quantity must be nonnegative" - else if Scalar.Quantity.compare applied_quantity quantity > 0 - then - Error "applied fill quantity exceeds the execution proposal" - 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* capacity = consume capacity applied_quantity in - let capacities = - Id.Instrument.Map.add instrument_id capacity capacities + match priced with + | None -> + let (Cursor next) = make_cursor capacities remaining in + next current_oms + | Some (price, price_attribution) -> + let* notional = Scalar.Money.notional price quantity in + let* fee_components, fee = + calculate_fee state ~instrument ~notional ~quantity + ~liquidity + ~fx_rates: + (List.map + (fun mark -> + (mark.Market_slice.currency, mark.rate)) + market_slice.fx_rates) in - Ok (make_cursor capacities remaining) - in - Ok (Proposed (proposed, continue))) + let proposed = + { + order_id = order.id; + quantity; + price; + fee; + fee_components; + liquidity; + executed_at; + price_attribution; + } + in + let continue applied_quantity = + if + Scalar.Quantity.compare applied_quantity + Scalar.Quantity.zero + < 0 + then Error "applied fill quantity must be nonnegative" + else if + Scalar.Quantity.compare applied_quantity quantity > 0 + then + Error + "applied fill quantity exceeds the execution proposal" + 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* capacity = consume capacity applied_quantity in + let capacities = + Id.Instrument.Map.add instrument_id capacity + capacities + in + Ok (make_cursor capacities remaining) + in + Ok (Proposed (proposed, continue)))) in Ok (make_cursor capacities eligible_order_ids) +let start_slice state = start_slice_with_policy Optimistic_touch state +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 + let finished market_ioc_orders = cursor (fun ~oms:_ -> Ok (Finished market_ioc_orders)) diff --git a/lib/execution.mli b/lib/execution.mli index e2d9f0e..c625232 100644 --- a/lib/execution.mli +++ b/lib/execution.mli @@ -1,6 +1,20 @@ (** Deterministic synchronized-slice execution simulation. *) type t +type missing_volume_policy = Reject_missing_volume | Zero_impact + +type cost_model = private { + half_spread_bps : int; + impact_coefficient_bps : int; + missing_volume_policy : missing_volume_policy; +} + +type price_attribution = private { + reference_price : Scalar.Price.t; + spread_adjustment : Scalar.Money.t; + impact_adjustment : Scalar.Money.t; + final_price : Scalar.Price.t; +} type proposed_fill = private { order_id : Id.Order.t; @@ -10,6 +24,7 @@ type proposed_fill = private { fee_components : Fee_schedule.calculated_component list; liquidity : Fee_schedule.liquidity; executed_at : Ptime.t; + price_attribution : price_attribution option; } type match_result = private { @@ -39,10 +54,19 @@ val create_v2 : fee_schedules:Fee_schedule.t list -> (t, string) result +val create_conservative : + participation_bps:int -> + fee_schedules:Fee_schedule.t list -> + half_spread_bps:int -> + impact_coefficient_bps:int -> + missing_volume_policy:missing_volume_policy -> + (t, string) result + val participation_bps : t -> int val fixed_fee : t -> Scalar.Money.t val fee_bps : t -> int val fee_schedules : t -> Fee_schedule.t list +val cost_model : t -> cost_model option val calculate_fee : t -> @@ -62,6 +86,20 @@ val start_slice : (** Start an immutable matching cursor from the orders eligible at the slice boundary. *) +val start_slice_next_open : + t -> + instruments:Instrument.t list -> + oms:Oms.t -> + Market_slice.t -> + (cursor, string) result + +val start_slice_adverse_touch : + 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 ad8097c..11b13d5 100644 --- a/lib/execution_model.ml +++ b/lib/execution_model.ml @@ -27,9 +27,26 @@ module Completed_bar_v1 = struct let start_slice = Execution.start_slice end +module Completed_bar_next_open_v1 = struct + let name = "completed_bar_next_open_v1" + let start_slice = Execution.start_slice_next_open +end + +module Completed_bar_adverse_touch_v1 = struct + let name = "completed_bar_adverse_touch_v1" + let start_slice = Execution.start_slice_adverse_touch +end + let of_module model = model let name (module Model : S) = Model.name -let builtins : t list = [ (module Completed_bar_v1) ] + +let builtins : t list = + [ + (module Completed_bar_v1); + (module Completed_bar_next_open_v1); + (module Completed_bar_adverse_touch_v1); + ] + let supported = List.map name builtins let completed_bar_v1_contract = @@ -37,7 +54,7 @@ let completed_bar_v1_contract = version = "2"; previous_versions = [ "1" ]; scenario_contract_versions = - [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ]; + [ "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" ]; @@ -54,9 +71,40 @@ let completed_bar_v1_contract = ]; } +let conservative_contract = + { + version = "1"; + previous_versions = []; + scenario_contract_versions = [ "13" ]; + required_fields = + [ + "version"; + "participation_bps"; + "fee_schedules"; + "spread_model"; + "impact_model"; + ]; + legacy_required_fields = []; + supported_order_types = [ "market"; "limit"; "stop"; "stop_limit" ]; + data_requirements = + [ "completed_ohlcv_bars"; "bar_volume_for_linear_impact" ]; + limits = + `Assoc + [ + ( "participation_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ( "half_spread_bps", + `Assoc [ ("minimum", `Int 0); ("maximum", `Int 10_000) ] ); + ( "impact_coefficient_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 | unsupported -> invalid_arg (Printf.sprintf "execution model %S has no configuration contract" @@ -98,10 +146,11 @@ let capabilities_to_yojson () = ("required_fields", strings contract.required_fields); ( "configuration_required_fields", `Assoc - [ - (contract.version, strings contract.required_fields); - ("1", strings contract.legacy_required_fields); - ] ); + ((contract.version, strings contract.required_fields) + :: List.map + (fun version -> + (version, strings contract.legacy_required_fields)) + contract.previous_versions) ); ("supported_order_types", strings contract.supported_order_types); ("data_requirements", strings contract.data_requirements); ("limits", contract.limits); diff --git a/lib/external_replay.ml b/lib/external_replay.ml index e72a720..ce795ce 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 "12" then + if String.equal contract_version "13" then + Engine.config_v13 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "12" then Engine.config_v12 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/market_slice.ml b/lib/market_slice.ml index 9f3fdbe..d50c61c 100644 --- a/lib/market_slice.ml +++ b/lib/market_slice.ml @@ -155,6 +155,8 @@ let create_v12 ~slice_sequence ~start_at ~end_at ~available_at ~received_at settlement_failures; } +let create_v13 = create_v12 + let create_v11 ~slice_sequence ~start_at ~end_at ~available_at ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations ~cash_rate_observations ~settlement_failures = diff --git a/lib/market_slice.mli b/lib/market_slice.mli index 329ecd1..6f08692 100644 --- a/lib/market_slice.mli +++ b/lib/market_slice.mli @@ -73,6 +73,21 @@ val create_v12 : lifecycle_events:Instrument_lifecycle.event list -> (t, string) result +val create_v13 : + 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 -> + (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 baff101..459c992 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 "12" then + if String.equal contract_version "13" then + Engine.config_v13 ~contract_version ~risk ~venue_calendars + ~execution_model ~execution ~financing ~settlement + ~max_internal_events + else if String.equal contract_version "12" then Engine.config_v12 ~contract_version ~risk ~venue_calendars ~execution_model ~execution ~financing ~settlement ~max_internal_events diff --git a/lib/scenario.ml b/lib/scenario.ml index 92fd715..734e1bd 100644 --- a/lib/scenario.ml +++ b/lib/scenario.ml @@ -554,7 +554,7 @@ 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 [ "12"; "11"; "10"; "9"; "8"; "7" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7" ] then parse_v7_risk base_currency instruments json else parse_legacy_risk base_currency instruments json @@ -654,7 +654,7 @@ let parse_fee_schedule instrument_ids json = Fee_schedule.create ~schedule_id ~instrument_id ~settlement_currency ~minimum ~maximum ~components -let parse_execution_v2 instruments fields = +let parse_execution_common instruments fields = let* participation_json = field fields "participation_bps" in let* participation_bps = integer ~name:"participation_bps" participation_json @@ -679,8 +679,68 @@ let parse_execution_v2 instruments fields = else Error "fee schedules must cover every configured instrument exactly once" in + Ok (participation_bps, schedules) + +let parse_execution_v2 instruments fields = + let* participation_bps, schedules = + parse_execution_common instruments fields + in Execution.create_v2 ~participation_bps ~fee_schedules:schedules +let parse_conservative_execution instruments fields = + let* participation_bps, fee_schedules = + parse_execution_common instruments fields + in + let* spread_json = field fields "spread_model" in + let* spread_fields = + object_fields ~name:"spread model" + ~expected:[ "model"; "half_spread_bps" ] + spread_json + in + let* spread_name = + Result.bind (field spread_fields "model") (string ~name:"spread model") + in + let* () = + if String.equal spread_name "fixed_half_spread_v1" then Ok () + else Error "unsupported spread model" + in + let* half_spread_bps = + Result.bind + (field spread_fields "half_spread_bps") + (integer ~name:"half_spread_bps") + in + let* impact_json = field fields "impact_model" in + let* impact_fields = + object_fields ~name:"impact model" + ~expected:[ "model"; "coefficient_bps"; "missing_volume_policy" ] + impact_json + in + let* impact_name = + Result.bind (field impact_fields "model") (string ~name:"impact model") + in + let* () = + if String.equal impact_name "linear_participation_v1" then Ok () + else Error "unsupported impact model" + in + let* impact_coefficient_bps = + Result.bind + (field impact_fields "coefficient_bps") + (integer ~name:"impact coefficient_bps") + in + let* missing_name = + Result.bind + (field impact_fields "missing_volume_policy") + (string ~name:"missing_volume_policy") + in + let* missing_volume_policy = + match missing_name with + | "reject" -> Ok Execution.Reject_missing_volume + | "zero_impact" -> Ok Execution.Zero_impact + | _ -> Error "missing_volume_policy must be reject or zero_impact" + in + Execution.create_conservative ~participation_bps ~fee_schedules + ~half_spread_bps ~impact_coefficient_bps ~missing_volume_policy + let parse_legacy_execution ~contract_version json = let* fields = object_fields ~name:"execution" @@ -739,14 +799,20 @@ let parse_versioned_execution ~contract_version ~instruments json = model_name) else let* execution = - if String.equal version "2" then + if + 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 parse_execution_values configuration in Ok (execution_model, execution) let parse_execution ~contract_version ~instruments json = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + if + List.mem contract_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then parse_versioned_execution ~contract_version ~instruments json else parse_legacy_execution ~contract_version json @@ -795,7 +861,9 @@ let parse_portfolio_intent ~name ~parse_target make json = Ok (make targets) let parse_submit_intent ~contract_version json = - let versioned = List.mem contract_version [ "12"; "11"; "10"; "9"; "8" ] in + let versioned = + List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8" ] + in let* fields = object_fields ~name:"submit_order intent" ~expected: @@ -1532,16 +1600,18 @@ let parse_cash_rate_observation json = let parse_slice ~contract_version json = let financing_fields = - if List.mem contract_version [ "12"; "11"; "10" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10" ] then [ "borrow_observations"; "cash_rate_observations" ] else [] in let settlement_fields = - if List.mem contract_version [ "12"; "11" ] then [ "settlement_failures" ] + if List.mem contract_version [ "13"; "12"; "11" ] then + [ "settlement_failures" ] else [] in let lifecycle_fields = - if String.equal contract_version "12" then [ "lifecycle_events" ] else [] + if List.mem contract_version [ "13"; "12" ] then [ "lifecycle_events" ] + else [] in let* fields = object_fields ~name:"market slice" @@ -1578,7 +1648,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 [ "12"; "11"; "10" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10" ] then let* borrow_json = Result.bind (field fields "borrow_observations") @@ -1593,7 +1663,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 [ "12"; "11" ] then + if List.mem contract_version [ "13"; "12"; "11" ] then let* failures_json = Result.bind (field fields "settlement_failures") @@ -1602,15 +1672,19 @@ let parse_slice ~contract_version json = let* settlement_failures = map_list parse_settlement_failure failures_json in - if String.equal contract_version "12" then + if List.mem contract_version [ "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 - Market_slice.create_v12 ~slice_sequence ~start_at ~end_at ~available_at - ~received_at ~bars ~fx_rates ~corporate_actions ~borrow_observations + 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 @@ -1655,7 +1729,9 @@ let construct_header ~root ~contract_path ~contract_version |> at (child root "base_currency") in let* initial_cash, initial_portfolio = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then + if + List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then let* portfolio = parse_initial_portfolio ~base_currency shape.initial_state |> at (child root "initial_portfolio") @@ -1724,7 +1800,8 @@ let construct_header ~root ~contract_path ~contract_version | _, _ -> Ok Financing.legacy_policy in let financing = - if List.mem contract_version [ "12"; "11"; "10" ] then Some financing + if List.mem contract_version [ "13"; "12"; "11"; "10" ] then + Some financing else None in let* settlement = diff --git a/lib/scenario_shape.ml b/lib/scenario_shape.ml index e385c8d..6888490 100644 --- a/lib/scenario_shape.ml +++ b/lib/scenario_shape.ml @@ -70,26 +70,28 @@ 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 [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then - "initial_portfolio" + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then "initial_portfolio" else "initial_cash" in let* initial_state = field ~root fields initial_field in let* instruments = field ~root fields "instruments" in let venue_calendars = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + if + List.mem contract_version + [ "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 [ "12"; "11"; "10" ] then + if List.mem contract_version [ "13"; "12"; "11"; "10" ] then List.assoc_opt "financing" fields else None in let settlement = - if List.mem contract_version [ "12"; "11" ] then + if List.mem contract_version [ "13"; "12"; "11" ] then List.assoc_opt "settlement" fields else None in @@ -120,13 +122,15 @@ let batch json = match preliminary with `String value -> value | _ -> "" in let calendar_fields = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + if + List.mem contract_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then - "initial_portfolio" + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then "initial_portfolio" else "initial_cash" in let* fields = @@ -146,11 +150,11 @@ let batch json = "slices"; ] @ calendar_fields - @ (if List.mem contract_version [ "12"; "11"; "10" ] then + @ (if List.mem contract_version [ "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "12"; "11" ] then [ "settlement" ] + if List.mem contract_version [ "13"; "12"; "11" ] then [ "settlement" ] else []) json in @@ -163,13 +167,15 @@ let batch json = let stream_header ~contract_version json = let root = "$.payload" in let calendar_fields = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + if + List.mem contract_version + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then [ "venue_calendars" ] else [] in let initial_field = - if List.mem contract_version [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then - "initial_portfolio" + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then "initial_portfolio" else "initial_cash" in let* fields = @@ -186,11 +192,11 @@ let stream_header ~contract_version json = "max_internal_events"; ] @ calendar_fields - @ (if List.mem contract_version [ "12"; "11"; "10" ] then + @ (if List.mem contract_version [ "13"; "12"; "11"; "10" ] then [ "financing" ] else []) @ - if List.mem contract_version [ "12"; "11" ] then [ "settlement" ] + if List.mem contract_version [ "13"; "12"; "11" ] then [ "settlement" ] else []) json in diff --git a/lib/scenario_validation.ml b/lib/scenario_validation.ml index 21b9668..527b0b8 100644 --- a/lib/scenario_validation.ml +++ b/lib/scenario_validation.ml @@ -48,8 +48,8 @@ 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 [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then - Ok () + if List.mem contract_version [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + then Ok () else Account.create ~base_currency ~initial_cash |> Result.map (fun _ -> ()) @@ -69,7 +69,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments let* () = if List.mem contract_version - [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5" ] then validate_venue_calendars ~root catalog venue_calendars else Ok () in @@ -89,7 +89,7 @@ let header ~root ~contract_version ~base_currency ~initial_cash ~instruments (child root (if List.mem contract_version - [ "12"; "11"; "10"; "9"; "8"; "7"; "6" ] + [ "13"; "12"; "11"; "10"; "9"; "8"; "7"; "6" ] then "initial_portfolio.cash" else "initial_cash")) "initial cash must contain every scenario currency exactly once" diff --git a/lib/strategy_protocol.ml b/lib/strategy_protocol.ml index 5f0790a..ece0f9a 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 [ "10"; "9"; "8"; "7"; "6"; "5" ] + List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6"; "5" ] let financing_to_yojson policy = `Assoc @@ -252,7 +252,43 @@ let execution_to_yojson ~protocol_version model execution = (Fee_schedule.components schedule)) ); ] in - if List.mem protocol_version [ "10"; "9"; "8"; "7" ] then + if + String.equal protocol_version "11" + && not (String.equal (Execution_model.name model) "completed_bar_v1") + then + let costs = Execution.cost_model execution |> Option.get in + `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)) ); + ( "spread_model", + `Assoc + [ + ("model", string "fixed_half_spread_v1"); + ("half_spread_bps", `Int costs.half_spread_bps); + ] ); + ( "impact_model", + `Assoc + [ + ("model", string "linear_participation_v1"); + ("coefficient_bps", `Int costs.impact_coefficient_bps); + ( "missing_volume_policy", + string + (match costs.missing_volume_policy with + | Execution.Reject_missing_volume -> "reject" + | Zero_impact -> "zero_impact") ); + ] ); + ] ); + ] + else if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7" ] then `Assoc [ ("model", string (Execution_model.name model)); @@ -291,6 +327,7 @@ let execution_to_yojson ~protocol_version model execution = let protocol_version initialization = match initialization.scenario_contract_version with + | "13" -> "11" | "12" -> "10" | "11" -> "9" | "10" -> "8" @@ -337,7 +374,7 @@ let initialize_message ~sequence:message_sequence initialization = ] in let fields = - if List.mem protocol_version [ "10"; "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then let initial_portfolio = Option.fold ~none:`Null ~some:Codec.initial_portfolio_to_yojson initialization.initial_portfolio @@ -350,14 +387,14 @@ let initialize_message ~sequence:message_sequence initialization = ( "venue_calendars", `List (List.map venue_calendar_to_yojson venue_calendars) ); ]; - (if List.mem protocol_version [ "10"; "9"; "8" ] then + (if List.mem protocol_version [ "11"; "10"; "9"; "8" ] then [ ( "financing", Option.fold ~none:`Null ~some:financing_to_yojson initialization.financing ); ] else []); - (if List.mem protocol_version [ "10"; "9" ] then + (if List.mem protocol_version [ "11"; "10"; "9" ] then [ ( "settlement", Option.fold ~none:`Null ~some:settlement_to_yojson @@ -391,14 +428,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 [ "10"; "9"; "8" ] then + @ (if List.mem protocol_version [ "11"; "10"; "9"; "8" ] then [ ("interest", money balance.interest); ("base_interest", money balance.base_interest); ] else []) @ - if List.mem protocol_version [ "10"; "9" ] then + if List.mem protocol_version [ "11"; "10"; "9" ] then [ ("settled_amount", money balance.settled_amount); ("unsettled_amount", money balance.unsettled_amount); @@ -418,7 +455,7 @@ let marked_position_to_yojson ~protocol_version ("weight", Option.fold ~none:`Null ~some:weight position.weight); ] @ - if List.mem protocol_version [ "10"; "9" ] then + if List.mem protocol_version [ "11"; "10"; "9" ] then [ ("settled_quantity", quantity position.settled_quantity); ("unsettled_quantity", quantity position.unsettled_quantity); @@ -501,8 +538,8 @@ let context_to_yojson ~protocol_version context = ( "working_orders", `List (List.map - (if List.mem protocol_version [ "10"; "9"; "8"; "7"; "6" ] then - Codec.order_to_yojson_v8 + (if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] + then Codec.order_to_yojson_v8 else Codec.order_to_yojson) working_orders) ); ("latest_bars", `List (List.map Codec.bar_to_yojson latest_bars)); @@ -514,7 +551,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "market_slice_closed"); ( "market_slice", - if String.equal protocol_version "10" then + if List.mem protocol_version [ "11"; "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 @@ -527,7 +564,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "fill_received"); ( "fill", - if List.mem protocol_version [ "10"; "9"; "8"; "7" ] then + if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7" ] then Codec.fill_to_yojson_v9 fill else Codec.fill_to_yojson fill ); ] @@ -536,7 +573,7 @@ let event_to_yojson ~protocol_version = function [ ("type", string "order_updated"); ( "order", - if List.mem protocol_version [ "10"; "9"; "8"; "7"; "6" ] then + if List.mem protocol_version [ "11"; "10"; "9"; "8"; "7"; "6" ] then Codec.order_to_yojson_v8 order else Codec.order_to_yojson order ); ] @@ -624,7 +661,8 @@ let parse_intents_payload ~protocol_version json = let* intent = Scenario.intent_of_yojson ~contract_version: - (if String.equal protocol_version "10" then "12" + (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" else if String.equal protocol_version "7" then "9" diff --git a/mkdocs.yml b/mkdocs.yml index c9145e4..7c61eb4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,14 +29,14 @@ nav: - Diagnostics: - Current v1: contracts/diagnostic/v1/README.md - Scenario and journal: - - Current v12: contracts/v12/README.md + - Current v13: contracts/v13/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 v10: contracts/strategy/v10/README.md + - Current v11: contracts/strategy/v11/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 a3d533b..671c25d 100755 --- a/scripts/check-deterministic-journals +++ b/scripts/check-deterministic-journals @@ -90,3 +90,11 @@ compare_journal \ v12-fill-clipped \ contracts/v12/fixtures/fill-clipped.scenario.json \ contracts/v12/fixtures/fill-clipped.journal.jsonl +compare_journal \ + v13-demo \ + contracts/v13/fixtures/demo.scenario.json \ + contracts/v13/fixtures/demo.journal.jsonl +compare_journal \ + v13-fill-clipped \ + contracts/v13/fixtures/fill-clipped.scenario.json \ + contracts/v13/fixtures/fill-clipped.journal.jsonl diff --git a/scripts/check-documentation.py b/scripts/check-documentation.py index bbe4a58..f6acfa2 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/v12/README.md", + "contracts/v13/README.md", "contracts/v5/README.md", "contracts/v4/README.md", "contracts/v3/README.md", "contracts/v2/README.md", "contracts/v1/README.md", - "contracts/strategy/v10/README.md", + "contracts/strategy/v11/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 3915dfe..6607931 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/v12/scenario.schema.json", - "share/trading_engine/contracts/v12/fixtures/demo.scenario.json", + "share/trading_engine/contracts/v13/scenario.schema.json", + "share/trading_engine/contracts/v13/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/v12/fixtures/demo.scenario.json", + "contracts/v13/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/v12/fixtures/demo.scenario.json", - "contracts/strategy/v10/message.schema.json", + "contracts/v13/fixtures/demo.scenario.json", + "contracts/strategy/v11/message.schema.json", ), epoch, ) @@ -412,7 +412,7 @@ def verify_release( "index.html", "docs/architecture/index.html", "contracts/v1/index.html", - "contracts/v12/scenario.schema.json", + "contracts/v13/scenario.schema.json", "api/trading_engine/Trading_engine/index.html", ), epoch, diff --git a/test/cli.t b/test/cli.t index a77dbbd..62a7360 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":["12","11","10","9","8","7","6","5","4","3"],"journal_contract_versions":["12","11","10","9","8","7","6","5","4","3"],"scenario_formats":["json","jsonl"],"journal_formats":["jsonl"],"execution_models":["completed_bar_v1"],"execution_model_contracts":[{"name":"completed_bar_v1","configuration_versions":["2","1"],"scenario_contract_versions":["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"}}}],"strategy_protocol_versions":["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":["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}} $ ../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 a0c266d..fa0aebb 100644 --- a/test/dune +++ b/test/dune @@ -70,6 +70,14 @@ ../contracts/v12/journal.schema.json ../contracts/v12/scenario-stream.schema.json ../contracts/v12/scenario.schema.json + ../contracts/v13/fixtures/demo.journal.jsonl + ../contracts/v13/fixtures/demo.scenario.json + ../contracts/v13/fixtures/demo.scenario.jsonl + ../contracts/v13/fixtures/fill-clipped.journal.jsonl + ../contracts/v13/fixtures/fill-clipped.scenario.json + ../contracts/v13/journal.schema.json + ../contracts/v13/scenario-stream.schema.json + ../contracts/v13/scenario.schema.json ../contracts/v6/fixtures/demo.scenario.json ../contracts/v6/fixtures/demo.scenario.jsonl ../contracts/v5/fixtures/demo.scenario.json @@ -85,6 +93,7 @@ ../contracts/strategy/v8/fixtures/external.strategy.jsonl ../contracts/strategy/v9/fixtures/external.strategy.jsonl ../contracts/strategy/v10/fixtures/external.strategy.jsonl + ../contracts/strategy/v11/fixtures/external.strategy.jsonl ../contracts/strategy/v4/fixtures/external.strategy.jsonl fake_strategy.py) (libraries @@ -103,6 +112,69 @@ (modules fuzz_protocol) (libraries trading_engine yojson unix)) +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v13/fixtures/demo.journal.jsonl + ../contracts/v13/fixtures/demo.scenario.json + ../contracts/v13/fixtures/demo.scenario.jsonl + ../contracts/v13/journal.schema.json + ../contracts/v13/scenario-stream.schema.json + ../contracts/v13/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v13/scenario.schema.json} + %{dep:../contracts/v13/scenario-stream.schema.json} + %{dep:../contracts/v13/journal.schema.json} + %{dep:../contracts/v13/fixtures/demo.scenario.json} + %{dep:../contracts/v13/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v13/fixtures/demo.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_schemas.py + ../contracts/v13/fixtures/fill-clipped.journal.jsonl + ../contracts/v13/fixtures/fill-clipped.scenario.json + ../contracts/v13/fixtures/demo.scenario.jsonl + ../contracts/v13/journal.schema.json + ../contracts/v13/scenario-stream.schema.json + ../contracts/v13/scenario.schema.json) + (action + (run + python3 + %{dep:validate_schemas.py} + %{dep:../contracts/v13/scenario.schema.json} + %{dep:../contracts/v13/scenario-stream.schema.json} + %{dep:../contracts/v13/journal.schema.json} + %{dep:../contracts/v13/fixtures/fill-clipped.scenario.json} + %{dep:../contracts/v13/fixtures/demo.scenario.jsonl} + %{dep:../contracts/v13/fixtures/fill-clipped.journal.jsonl}))) + +(rule + (alias runtest) + (deps + validate_strategy_schema.py + ../contracts/v13/scenario.schema.json + ../contracts/v13/journal.schema.json + ../contracts/diagnostic/v1/diagnostic.schema.json + ../contracts/strategy/v11/message.schema.json + ../contracts/strategy/v11/transcript.schema.json + ../contracts/strategy/v11/fixtures/external.strategy.jsonl) + (action + (run + python3 + %{dep:validate_strategy_schema.py} + %{dep:../contracts/v13/scenario.schema.json} + %{dep:../contracts/v13/journal.schema.json} + %{dep:../contracts/diagnostic/v1/diagnostic.schema.json} + %{dep:../contracts/strategy/v11/message.schema.json} + %{dep:../contracts/strategy/v11/transcript.schema.json} + %{dep:../contracts/strategy/v11/fixtures/external.strategy.jsonl}))) + (rule (alias runtest) (deps diff --git a/test/test_diagnostic.ml b/test/test_diagnostic.ml index 4e7a482..fcac251 100644 --- a/test/test_diagnostic.ml +++ b/test/test_diagnostic.ml @@ -91,13 +91,30 @@ let capabilities_publish_versioned_resource_limits () = (T.Diagnostic.code_to_string T.Diagnostic.Resource_limit) let capabilities_describe_execution_contracts () = - let model = + let models = match T.Contract.capabilities_to_yojson () |> field "execution_model_contracts" with - | `List [ model ] -> model - | _ -> Alcotest.fail "expected one execution-model capability" + | `List models -> models + | _ -> Alcotest.fail "expected execution-model capabilities" in + let names = + List.map + (fun model -> + match field "name" model with + | `String value -> value + | _ -> Alcotest.fail "expected execution-model name") + models + in + Alcotest.(check (list string)) + "stable model catalog" + [ + "completed_bar_v1"; + "completed_bar_next_open_v1"; + "completed_bar_adverse_touch_v1"; + ] + names; + let model = List.hd models in Alcotest.(check string) "stable model name" "completed_bar_v1" (match field "name" model with @@ -118,7 +135,7 @@ let capabilities_describe_execution_contracts () = (strings "configuration_versions"); Alcotest.(check (list string)) "scenario contracts" - [ "12"; "11"; "10"; "9"; "8"; "7"; "6"; "5"; "4"; "3" ] + [ "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_execution.ml b/test/test_execution.ml index b8a7503..bcae7e6 100644 --- a/test/test_execution.ml +++ b/test/test_execution.ml @@ -11,6 +11,258 @@ let match_orders ?(configured = instrument ()) ?(engine = execution ()) ~oms T.Execution.match_slice engine ~instruments:[ configured ] ~oms market_slice |> ok +let conservative_execution ?(half_spread_bps = 0) ?(impact_coefficient_bps = 0) + ?(missing_volume_policy = T.Execution.Reject_missing_volume) () = + let component = + T.Fee_schedule.create_component ~name:"broker" ~currency:"USD" + ~basis:(T.Fee_schedule.Fixed (money "0.1")) + ~rounding:T.Fee_schedule.Up ~applicability:T.Fee_schedule.Any + |> ok + in + let schedule = + T.Fee_schedule.create ~schedule_id:"test-fees-v1" + ~instrument_id:(instrument_id "test-equity") + ~settlement_currency:"USD" ~minimum:None ~maximum:None + ~components:[ component ] + |> ok + in + T.Execution.create_conservative ~participation_bps:10_000 + ~fee_schedules:[ schedule ] ~half_spread_bps ~impact_coefficient_bps + ~missing_volume_policy + |> ok + +let conservative_step start ?(kind = T.Order.Market) ?(side = T.Order.Buy) + ?(slice = market_slice 2L) engine = + let oms, _ = oms_with_order (request ~kind ~side ()) in + let cursor = start engine ~instruments:[ instrument () ] ~oms slice |> ok in + T.Execution.next cursor ~oms |> ok + +let conservative_limit_models_diverge () = + let engine = conservative_execution () in + let limit = T.Order.Limit (price "100") in + let touch = + market_slice + ~bars:[ bar ~open_price:"105" ~high_price:"110" ~low_price:"100" 2L ] + 2L + in + (match + conservative_step T.Execution.start_slice_next_open ~kind:limit + ~slice:touch engine + with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "next-open model filled an intrabar touch"); + (match + conservative_step T.Execution.start_slice_adverse_touch ~kind:limit + ~slice:touch engine + with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "adverse-touch model filled without trade-through"); + let traded_through = + market_slice + ~bars:[ bar ~open_price:"105" ~high_price:"110" ~low_price:"99.99" 2L ] + 2L + in + match + conservative_step T.Execution.start_slice_adverse_touch ~kind:limit + ~slice:traded_through engine + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "one-tick adverse reference" (price "99.99") + proposal.price + | _ -> Alcotest.fail "adverse trade-through did not fill" + +let conservative_costs_are_tick_aligned_and_attributed () = + let engine = + conservative_execution ~half_spread_bps:10 ~impact_coefficient_bps:100 () + in + match + conservative_step T.Execution.start_slice_next_open + ~slice: + (market_slice + ~bars:[ bar ~open_price:"100" ~volume:(Some "100") 2L ] + 2L) + engine + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "spread and impact final price" + (price "100.2") proposal.price; + let attribution = Option.get proposal.price_attribution in + Alcotest.check price_testable "reference" (price "100") + attribution.reference_price; + Alcotest.check money_testable "spread" (money "0.1") + attribution.spread_adjustment; + Alcotest.check money_testable "impact" (money "0.1") + attribution.impact_adjustment; + Alcotest.check price_testable "attributed final" proposal.price + attribution.final_price + | _ -> Alcotest.fail "expected conservative market fill" + +let conservative_missing_volume_policy_is_explicit () = + let missing = market_slice ~bars:[ bar ~volume:None 2L ] 2L in + let rejecting = conservative_execution ~impact_coefficient_bps:100 () in + let oms, _ = oms_with_order (request ()) in + let cursor = + T.Execution.start_slice_next_open rejecting + ~instruments:[ instrument () ] + ~oms missing + |> ok + in + Alcotest.(check bool) + "missing volume rejected" true + (Result.is_error (T.Execution.next cursor ~oms)); + let zero = + conservative_execution ~half_spread_bps:10 ~impact_coefficient_bps:100 + ~missing_volume_policy:T.Execution.Zero_impact () + in + match + conservative_step T.Execution.start_slice_next_open ~slice:missing zero + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "zero-impact fallback keeps spread" + (price "100.1") proposal.price + | _ -> Alcotest.fail "zero-impact fallback did not fill" + +let conservative_configuration_is_bounded () = + let valid = conservative_execution () in + let schedules = T.Execution.fee_schedules valid in + let create half_spread_bps impact_coefficient_bps = + T.Execution.create_conservative ~participation_bps:10_000 + ~fee_schedules:schedules ~half_spread_bps ~impact_coefficient_bps + ~missing_volume_policy:T.Execution.Reject_missing_volume + in + List.iter + (fun (spread, impact) -> + Alcotest.(check bool) + "out-of-range cost rejected" true + (Result.is_error (create spread impact))) + [ (-1, 0); (10_001, 0); (0, -1); (0, 10_001) ]; + Alcotest.(check bool) + "v2 participation bound enforced" true + (Result.is_error + (T.Execution.create_v2 ~participation_bps:(-1) ~fee_schedules:schedules)); + let schedule = List.hd schedules in + Alcotest.(check bool) + "duplicate fee schedules rejected" true + (Result.is_error + (T.Execution.create_v2 ~participation_bps:10_000 + ~fee_schedules:[ schedule; schedule ])); + Alcotest.(check bool) + "missing instrument fee schedule rejected" true + (Result.is_error + (T.Execution.calculate_fee valid + ~instrument:(instrument ~id:"other-equity" ~symbol:"OTHER" ()) + ~notional:(money "100") ~quantity:(quantity "1") + ~liquidity:T.Fee_schedule.Taker + ~fx_rates:[ ("USD", price "1") ])) + +let conservative_sell_costs_and_limit_protection () = + let engine = + conservative_execution ~half_spread_bps:10 ~impact_coefficient_bps:100 () + in + (match + conservative_step T.Execution.start_slice_next_open ~side:T.Order.Sell + ~slice: + (market_slice + ~bars:[ bar ~open_price:"100" ~volume:(Some "100") 2L ] + 2L) + engine + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "sell costs reduce execution price" + (price "99.8") proposal.price; + let attribution = Option.get proposal.price_attribution in + Alcotest.check money_testable "sell spread attribution" (money "0.1") + attribution.spread_adjustment; + Alcotest.check money_testable "sell impact attribution" (money "0.1") + attribution.impact_adjustment + | _ -> Alcotest.fail "expected conservative sell fill"); + let buy_limit = T.Order.Limit (price "100") in + match + conservative_step T.Execution.start_slice_next_open ~kind:buy_limit + ~slice: + (market_slice + ~bars:[ bar ~open_price:"100" ~volume:(Some "100") 2L ] + 2L) + engine + with + | T.Execution.Finished _ -> ( + let buy_with_room = T.Order.Limit (price "101") in + (match + conservative_step T.Execution.start_slice_next_open ~kind:buy_with_room + ~slice: + (market_slice + ~bars:[ bar ~open_price:"100" ~volume:(Some "100") 2L ] + 2L) + engine + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "cost-adjusted buy respects limit" + (price "100.2") proposal.price + | _ -> Alcotest.fail "buy with limit room did not fill"); + let sell_limit = T.Order.Limit (price "100") in + (match + conservative_step T.Execution.start_slice_next_open ~kind:sell_limit + ~side:T.Order.Sell + ~slice: + (market_slice + ~bars:[ bar ~open_price:"100" ~volume:(Some "100") 2L ] + 2L) + engine + with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "cost-adjusted fill violated sell limit"); + let sell_with_room = T.Order.Limit (price "99") in + match + conservative_step T.Execution.start_slice_next_open ~kind:sell_with_room + ~side:T.Order.Sell + ~slice: + (market_slice + ~bars:[ bar ~open_price:"100" ~volume:(Some "100") 2L ] + 2L) + engine + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "cost-adjusted sell respects limit" + (price "99.8") proposal.price + | _ -> Alcotest.fail "sell with limit room did not fill") + | _ -> Alcotest.fail "cost-adjusted fill violated buy limit" + +let conservative_adverse_sell_requires_trade_through () = + let engine = conservative_execution () in + let limit = T.Order.Limit (price "100") in + let touch = + market_slice + ~bars: + [ + bar ~open_price:"95" ~high_price:"100" ~low_price:"90" + ~close_price:"95" 2L; + ] + 2L + in + (match + conservative_step T.Execution.start_slice_adverse_touch ~kind:limit + ~side:T.Order.Sell ~slice:touch engine + with + | T.Execution.Finished _ -> () + | _ -> Alcotest.fail "sell filled without one-tick trade-through"); + let traded_through = + market_slice + ~bars: + [ + bar ~open_price:"95" ~high_price:"100.01" ~low_price:"90" + ~close_price:"95" 2L; + ] + 2L + in + match + conservative_step T.Execution.start_slice_adverse_touch ~kind:limit + ~side:T.Order.Sell ~slice:traded_through engine + with + | T.Execution.Proposed (proposal, _) -> + Alcotest.check price_testable "sell adverse reference" (price "100.01") + proposal.price + | _ -> Alcotest.fail "sell trade-through did not fill" + let single_order_match ?(side = T.Order.Buy) ?(kind = T.Order.Market) ?(quantity_value = "10") ?(slice = market_slice 2L) () = let request = request ~side ~kind ~quantity_value () in @@ -406,6 +658,18 @@ let incomplete_market_slice_returns_error () = let tests = [ + Alcotest.test_case "conservative limit models diverge" `Quick + conservative_limit_models_diverge; + Alcotest.test_case "conservative costs are attributed" `Quick + conservative_costs_are_tick_aligned_and_attributed; + Alcotest.test_case "conservative missing-volume policy" `Quick + conservative_missing_volume_policy_is_explicit; + Alcotest.test_case "conservative configuration bounds" `Quick + conservative_configuration_is_bounded; + Alcotest.test_case "conservative sell costs and limits" `Quick + conservative_sell_costs_and_limit_protection; + Alcotest.test_case "conservative adverse sell" `Quick + conservative_adverse_sell_requires_trade_through; Alcotest.test_case "order waits for later slice" `Quick order_waits_for_later_slice; Alcotest.test_case "order waits for causal slice time" `Quick diff --git a/test/test_reducer.ml b/test/test_reducer.ml index 4532e35..38899d0 100644 --- a/test/test_reducer.ml +++ b/test/test_reducer.ml @@ -536,6 +536,32 @@ let configured_execution_model_is_dispatched () = "selected model is audited" No_fill_execution.name actual | _ -> Alcotest.fail "expected run start" +let execution_model_configuration_must_match () = + let completed = T.Execution_model.find "completed_bar_v1" |> ok in + let next_open = T.Execution_model.find "completed_bar_next_open_v1" |> ok in + let conservative = + T.Execution.create_conservative ~participation_bps:10_000 ~fee_schedules:[] + ~half_spread_bps:0 ~impact_coefficient_bps:0 + ~missing_volume_policy:T.Execution.Reject_missing_volume + |> ok + in + let configure contract_version execution_model execution = + T.Engine.config ~contract_version ~risk:(risk ()) ~execution_model + ~execution ~max_internal_events:1000 + in + Alcotest.(check bool) + "conservative model requires pricing configuration" true + (Result.is_error (configure "13" next_open (execution ()))); + Alcotest.(check bool) + "legacy model rejects conservative pricing" true + (Result.is_error (configure "13" completed conservative)); + Alcotest.(check bool) + "conservative model is v13-only" true + (Result.is_error (configure "12" next_open conservative)); + Alcotest.(check bool) + "matching conservative configuration accepted" true + (Result.is_ok (configure "13" next_open conservative)) + module Cancel_next_strategy = struct type state = { submitted : bool; cancelled : bool } @@ -829,6 +855,8 @@ let tests = Alcotest.test_case "one valuation per slice" `Quick one_valuation_per_slice; Alcotest.test_case "configured execution model is dispatched" `Quick configured_execution_model_is_dispatched; + Alcotest.test_case "execution model configuration matches" `Quick + execution_model_configuration_must_match; Alcotest.test_case "callbacks use current slice and synchronous responses" `Quick callbacks_use_current_slice_and_apply_responses_before_matching; Alcotest.test_case "interactive reducer matches scripted strategy" `Quick diff --git a/test/test_scenario.ml b/test/test_scenario.ml index c3c91ad..76306b3 100644 --- a/test/test_scenario.ml +++ b/test/test_scenario.ml @@ -2,12 +2,12 @@ open Test_support module T = Trading_engine let demo_document () = - In_channel.with_open_bin "../contracts/v12/fixtures/demo.scenario.json" + In_channel.with_open_bin "../contracts/v13/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/v12/fixtures/demo.scenario.jsonl" +let stream_path = "../contracts/v13/fixtures/demo.scenario.jsonl" let stream_document () = In_channel.with_open_bin stream_path In_channel.input_all @@ -75,7 +75,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_v12 ~slice_sequence:(Int64.of_int index) + T.Market_slice.create_v13 ~slice_sequence:(Int64.of_int index) ~start_at ~end_at:(add_seconds base (offset + 1)) ~available_at:(add_seconds base (offset + 2)) @@ -95,7 +95,7 @@ let write_large_stream path slice_count = let payload = `Assoc [ - ("market_slice", T.Codec.market_slice_to_yojson_v12 market_slice); + ("market_slice", T.Codec.market_slice_to_yojson_v13 market_slice); ("intents", `List []); ] in @@ -116,7 +116,7 @@ let demo_contract_parses () = Alcotest.(check int) "one instrument" 1 (List.length scenario.instruments); Alcotest.(check int) "four slices" 4 (List.length scenario.slices); Alcotest.(check string) - "execution model" "completed_bar_v1" + "execution model" "completed_bar_adverse_touch_v1" (T.Execution_model.name scenario.execution_model); match scenario.metadata with | `Assoc fields -> @@ -140,9 +140,9 @@ let schema_artifacts_parse () = (List.mem_assoc "$defs" fields) | _ -> Alcotest.fail (path ^ " must contain a JSON object") in - check_schema "../contracts/v12/scenario.schema.json"; - check_schema "../contracts/v12/scenario-stream.schema.json"; - check_schema "../contracts/v12/journal.schema.json" + check_schema "../contracts/v13/scenario.schema.json"; + check_schema "../contracts/v13/scenario-stream.schema.json"; + check_schema "../contracts/v13/journal.schema.json" let timestamp_precision_is_bounded () = List.iter @@ -203,7 +203,7 @@ let v12_distributions_and_lifecycle_parse () = |> ok in let market_slice = - T.Market_slice.create_v12 ~slice_sequence:1L + T.Market_slice.create_v13 ~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") @@ -341,7 +341,7 @@ let v12_distributions_and_lifecycle_parse () = | _ -> Alcotest.fail "demo slice must be an object" in `List - (T.Codec.market_slice_to_yojson_v12 market_slice + (T.Codec.market_slice_to_yojson_v13 market_slice :: List.map add_child_bar rest) | _ -> Alcotest.fail "demo slices must be nonempty" in @@ -431,8 +431,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 12, 11, 10, \ - 9, 8, 7, 6, 5, 4, 3)" + "unsupported scenario contract_version \"2\" (expected one of 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 +582,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_v12 ~slice_sequence:(Int64.of_int index) ~start_at + T.Market_slice.create_v13 ~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)) @@ -596,7 +596,7 @@ let dense_schedule_document slice_count = ~corporate_actions:[] ~borrow_observations:[ borrow_observation ] ~cash_rate_observations:[ cash_rate_observation ] ~settlement_failures:[] ~lifecycle_events:[] - |> ok |> T.Codec.market_slice_to_yojson_v12) + |> ok |> T.Codec.market_slice_to_yojson_v13) in let schedule = List.init slice_count (fun offset -> @@ -1012,7 +1012,7 @@ let execution_model_is_required_and_supported () = Alcotest.(check string) "unsupported model/version diagnosed" "unsupported execution configuration version \"99\" for model \ - \"completed_bar_v1\"" + \"completed_bar_adverse_touch_v1\"" (T.Scenario.of_yojson unsupported_version |> diagnostic_message); let extra_configuration = change_configuration (function @@ -1021,7 +1021,54 @@ let execution_model_is_required_and_supported () = in Alcotest.(check bool) "model configuration is strict" true - (Result.is_error (T.Scenario.of_yojson extra_configuration)) + (Result.is_error (T.Scenario.of_yojson extra_configuration)); + let unsupported_spread = + change_configuration + (map_field "spread_model" + (change_field "model" (`String "future_spread"))) + in + Alcotest.(check string) + "spread model is explicit" "unsupported spread model" + (T.Scenario.of_yojson unsupported_spread |> diagnostic_message); + let unsupported_impact = + change_configuration + (map_field "impact_model" + (change_field "model" (`String "future_impact"))) + in + Alcotest.(check string) + "impact model is explicit" "unsupported impact model" + (T.Scenario.of_yojson unsupported_impact |> diagnostic_message); + let invalid_missing_volume = + change_configuration + (map_field "impact_model" + (change_field "missing_volume_policy" (`String "estimate"))) + in + Alcotest.(check string) + "missing-volume policy is explicit" + "missing_volume_policy must be reject or zero_impact" + (T.Scenario.of_yojson invalid_missing_volume |> diagnostic_message); + let zero_impact = + change_configuration + (map_field "impact_model" + (change_field "missing_volume_policy" (`String "zero_impact"))) + in + Alcotest.(check bool) + "zero-impact policy parses" true + (Result.is_ok (T.Scenario.of_yojson zero_impact)); + let invalid_spread_bps = + change_configuration + (map_field "spread_model" (change_field "half_spread_bps" (`Int 10_001))) + in + Alcotest.(check bool) + "spread bound enforced" true + (Result.is_error (T.Scenario.of_yojson invalid_spread_bps)); + let invalid_impact_bps = + change_configuration + (map_field "impact_model" (change_field "coefficient_bps" (`Int 10_001))) + in + Alcotest.(check bool) + "impact bound enforced" true + (Result.is_error (T.Scenario.of_yojson invalid_impact_bps)) let deterministic_replay () = let scenario = demo () in @@ -1084,13 +1131,17 @@ let audit_ids_are_deterministic_and_causal () = [ "demo-event-000000000004"; "demo-event-000000000006" ] (cause_strings (event 8L)); Alcotest.(check (list string)) - "fill cites order creation and executable slice" + "price selection cites order creation and executable slice" [ "demo-event-000000000008"; "demo-event-000000000010" ] (cause_strings (event 12L)); + Alcotest.(check (list string)) + "fill cites price selection" + [ "demo-event-000000000012" ] + (cause_strings (event 13L)); Alcotest.(check (list string)) "completion cites terminal valuation" - [ "demo-event-000000000025" ] - (cause_strings (event 26L)); + [ "demo-event-000000000028" ] + (cause_strings (event 29L)); match (event 8L).event with | T.Audit.Order_accepted order -> Alcotest.(check string) @@ -1109,14 +1160,15 @@ let replay_ends_with_completion_summary () = (match first.event with | T.Audit.Run_started { scenario_sha256 = actual; execution_model } -> Alcotest.(check string) "start hash" hash actual; - Alcotest.(check string) "start model" "completed_bar_v1" execution_model + Alcotest.(check string) + "start model" "completed_bar_adverse_touch_v1" execution_model | _ -> Alcotest.fail "expected run start"); match completion.event with | T.Audit.Run_completed { scenario_sha256 = actual; execution_model; valuation; _ } -> Alcotest.(check string) "completion hash" hash actual; Alcotest.(check string) - "completion model" "completed_bar_v1" execution_model; + "completion model" "completed_bar_adverse_touch_v1" execution_model; Alcotest.check money_testable "summary equity" result.valuation.equity valuation.account.equity | _ -> Alcotest.fail "expected run completion payload" @@ -1128,7 +1180,7 @@ let replay_matches_golden_file () = |> fun value -> value ^ "\n" in let expected = - In_channel.with_open_bin "../contracts/v12/fixtures/demo.journal.jsonl" + In_channel.with_open_bin "../contracts/v13/fixtures/demo.journal.jsonl" In_channel.input_all in Alcotest.(check string) "stable audit contract" expected actual @@ -1156,7 +1208,7 @@ let v3_replay_matches_frozen_golden_file () = let fill_clipping_fixture_reconciles () = let document = In_channel.with_open_bin - "../contracts/v12/fixtures/fill-clipped.scenario.json" + "../contracts/v13/fixtures/fill-clipped.scenario.json" In_channel.input_all in let scenario = T.Scenario.of_string document |> ok in @@ -1170,7 +1222,7 @@ let fill_clipping_fixture_reconciles () = in let expected = In_channel.with_open_bin - "../contracts/v12/fixtures/fill-clipped.journal.jsonl" + "../contracts/v13/fixtures/fill-clipped.journal.jsonl" In_channel.input_all in Alcotest.(check string) "fill clipping audit reconciliation" expected actual @@ -1270,8 +1322,8 @@ let streamed_replay_matches_batch_semantics () = Alcotest.(check int64) "four streamed slices" 4L result.slice_count; Alcotest.(check int64) "two schedule batches" 2L result.schedule_count; Alcotest.(check int) "one instrument" 1 result.instrument_count; - Alcotest.(check int64) "thirty-one audits" 31L result.audit_count; - Alcotest.check money_testable "same equity" (money "10111.946958") + Alcotest.(check int64) "twenty-nine audits" 29L result.audit_count; + Alcotest.check money_testable "same equity" (money "10111.979929") result.valuation.equity; Alcotest.(check string) "stream and batch journals agree" expected diff --git a/test/test_strategy_protocol.ml b/test/test_strategy_protocol.ml index 9eab6ed..a0fe99c 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" "10" + "protocol version" "11" (match field "strategy_protocol_version" message with | `String value -> value | _ -> Alcotest.fail "expected version string"); @@ -105,6 +105,49 @@ let initialize_message_includes_calendars () = | _ -> Alcotest.fail "expected calendar ID") | _ -> Alcotest.fail "expected one serialized venue calendar" +let conservative_initialize_message_encodes_cost_models () = + let check model_name policy expected_policy = + let base = initialization () in + let execution = + T.Execution.create_conservative ~participation_bps:7500 + ~fee_schedules:(T.Execution.fee_schedules base.execution) + ~half_spread_bps:7 ~impact_coefficient_bps:23 + ~missing_volume_policy:policy + |> ok + in + let execution_model = T.Execution_model.find model_name |> ok in + let message = + T.Strategy_protocol.initialize_message ~sequence:1L + { base with execution_model; execution } + in + let encoded = field "payload" message |> field "execution" in + Alcotest.(check string) + "conservative model name" model_name + (match field "model" encoded with + | `String value -> value + | _ -> Alcotest.fail "expected execution model"); + let configuration = field "configuration" encoded in + Alcotest.(check int) + "half spread" 7 + (match field "spread_model" configuration |> field "half_spread_bps" with + | `Int value -> value + | _ -> Alcotest.fail "expected half spread"); + Alcotest.(check int) + "impact coefficient" 23 + (match field "impact_model" configuration |> field "coefficient_bps" with + | `Int value -> value + | _ -> Alcotest.fail "expected impact coefficient"); + Alcotest.(check string) + "missing volume policy" expected_policy + (match + field "impact_model" configuration |> field "missing_volume_policy" + with + | `String value -> value + | _ -> Alcotest.fail "expected missing-volume policy") + in + check "completed_bar_next_open_v1" T.Execution.Reject_missing_volume "reject"; + check "completed_bar_adverse_touch_v1" T.Execution.Zero_impact "zero_impact" + let legacy_initialize_message_remains_frozen () = let initialization = { @@ -222,7 +265,7 @@ let nonpositive_equity_omits_weights () = let response message_type payload = `Assoc [ - ("strategy_protocol_version", `String "10"); + ("strategy_protocol_version", `String "11"); ("strategy_sequence", `String "3"); ("message_type", `String message_type); ("payload", payload); @@ -494,6 +537,8 @@ let tests = initialize_message_is_complete; Alcotest.test_case "initialize message includes calendars" `Quick initialize_message_includes_calendars; + Alcotest.test_case "conservative initialization encodes costs" `Quick + conservative_initialize_message_encodes_cost_models; Alcotest.test_case "legacy initialize message remains frozen" `Quick legacy_initialize_message_remains_frozen; Alcotest.test_case "event context is complete" `Quick